diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
commit | 55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch) | |
tree | ecd18b995aea8eeeb8b3823266280d41245bf0f7 /ddms/libs/ddmuilib/src | |
parent | 82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff) | |
download | sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'ddms/libs/ddmuilib/src')
97 files changed, 19685 insertions, 0 deletions
diff --git a/ddms/libs/ddmuilib/src/Android.mk b/ddms/libs/ddmuilib/src/Android.mk new file mode 100644 index 0000000..acbda44 --- /dev/null +++ b/ddms/libs/ddmuilib/src/Android.mk @@ -0,0 +1,22 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAVA_RESOURCE_DIRS := resources + +LOCAL_JAVA_LIBRARIES := \ + ddmlib \ + swt \ + org.eclipse.jface_3.2.0.I20060605-1400 \ + org.eclipse.equinox.common_3.2.0.v20060603 \ + org.eclipse.core.commands_3.2.0.I20060605-1400 \ + jcommon-1.0.12 \ + jfreechart-1.0.9 \ + jfreechart-1.0.9-swt + +LOCAL_MODULE := ddmuilib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java new file mode 100644 index 0000000..a2f12d5 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java @@ -0,0 +1,278 @@ +/* + * 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 com.android.ddmlib.*; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.HashMap; + +/** + * Represents an addr2line process to get filename/method information from a + * memory address.<br> + * Each process can only handle one library, which should be provided when + * creating a new process.<br> + * <br> + * The processes take some time to load as they need to parse the library files. + * For this reason, processes cannot be manually started. Instead the class + * keeps an internal list of processes and one asks for a process for a specific + * library, using <code>getProcess(String library)<code>.<br></br> + * Internally, the processes are started in pipe mode to be able to query them + * with multiple addresses. + */ +public class Addr2Line { + + /** + * Loaded processes list. This is also used as a locking object for any + * methods dealing with starting/stopping/creating processes/querying for + * method. + */ + private static final HashMap<String, Addr2Line> sProcessCache = + new HashMap<String, Addr2Line>(); + + /** + * byte array representing a carriage return. Used to push addresses in the + * process pipes. + */ + private static final byte[] sCrLf = { + '\n' + }; + + /** Path to the library */ + private String mLibrary; + + /** the command line process */ + private Process mProcess; + + /** buffer to read the result of the command line process from */ + private BufferedReader mResultReader; + + /** + * output stream to provide new addresses to decode to the command line + * process + */ + private BufferedOutputStream mAddressWriter; + + /** + * Returns the instance of a Addr2Line process for the specified library. + * <br>The library should be in a format that makes<br> + * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file. + * + * @param library the library in which to look for addresses. + * @return a new Addr2Line object representing a started process, ready to + * be queried for addresses. If any error happened when launching a + * new process, <code>null</code> will be returned. + */ + public static Addr2Line getProcess(final String library) { + // synchronize around the hashmap object + if (library != null) { + synchronized (sProcessCache) { + // look for an existing process + Addr2Line process = sProcessCache.get(library); + + // if we don't find one, we create it + if (process == null) { + process = new Addr2Line(library); + + // then we start it + boolean status = process.start(); + + if (status) { + // if starting the process worked, then we add it to the + // list. + sProcessCache.put(library, process); + } else { + // otherwise we just drop the object, to return null + process = null; + } + } + // return the process + return process; + } + } + return null; + } + + /** + * Construct the object with a library name. + * <br>The library should be in a format that makes<br> + * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file. + * + * @param library the library in which to look for address. + */ + private Addr2Line(final String library) { + mLibrary = library; + } + + /** + * Starts the command line process. + * + * @return true if the process was started, false if it failed to start, or + * if there was any other errors. + */ + private boolean start() { + // because this is only called from getProcess() we know we don't need + // to synchronize this code. + + // get the output directory. + String symbols = DdmUiPreferences.getSymbolDirectory(); + + // build the command line + String[] command = new String[5]; + command[0] = DdmUiPreferences.getAddr2Line(); + command[1] = "-C"; + command[2] = "-f"; + command[3] = "-e"; + command[4] = symbols + mLibrary.replaceAll("libc\\.so", "libc_debug\\.so"); + + try { + // attempt to start the process + mProcess = Runtime.getRuntime().exec(command); + + if (mProcess != null) { + // get the result reader + InputStreamReader is = new InputStreamReader(mProcess + .getInputStream()); + mResultReader = new BufferedReader(is); + + // get the outstream to write the addresses + mAddressWriter = new BufferedOutputStream(mProcess + .getOutputStream()); + + // check our streams are here + if (mResultReader == null || mAddressWriter == null) { + // not here? stop the process and return false; + mProcess.destroy(); + mProcess = null; + return false; + } + + // return a success + return true; + } + + } catch (IOException e) { + // log the error + String msg = String.format( + "Error while trying to start %1$s process for library %2$s", + DdmUiPreferences.getAddr2Line(), mLibrary); + Log.e("ddm-Addr2Line", msg); + + // drop the process just in case + if (mProcess != null) { + mProcess.destroy(); + mProcess = null; + } + } + + // we can be here either cause the allocation of mProcess failed, or we + // caught an exception + return false; + } + + /** + * Stops the command line process. + */ + public void stop() { + synchronized (sProcessCache) { + if (mProcess != null) { + // remove the process from the list + sProcessCache.remove(mLibrary); + + // then stops the process + mProcess.destroy(); + + // set the reference to null. + // this allows to make sure another thread calling getAddress() + // will not query a stopped thread + mProcess = null; + } + } + } + + /** + * Stops all current running processes. + */ + public static void stopAll() { + // because of concurrent access (and our use of HashMap.values()), we + // can't rely on the synchronized inside stop(). We need to put one + // around the whole loop. + synchronized (sProcessCache) { + // just a basic loop on all the values in the hashmap and call to + // stop(); + Collection<Addr2Line> col = sProcessCache.values(); + for (Addr2Line a2l : col) { + a2l.stop(); + } + } + } + + /** + * Looks up an address and returns method name, source file name, and line + * number. + * + * @param addr the address to look up + * @return a BacktraceInfo object containing the method/filename/linenumber + * or null if the process we stopped before the query could be + * processed, or if an IO exception happened. + */ + public NativeStackCallInfo getAddress(long addr) { + // even though we don't access the hashmap object, we need to + // synchronized on it to prevent + // another thread from stopping the process we're going to query. + synchronized (sProcessCache) { + // check the process is still alive/allocated + if (mProcess != null) { + // prepare to the write the address to the output buffer. + + // first, conversion to a string containing the hex value. + String tmp = Long.toString(addr, 16); + + try { + // write the address to the buffer + mAddressWriter.write(tmp.getBytes()); + + // add CR-LF + mAddressWriter.write(sCrLf); + + // flush it all. + mAddressWriter.flush(); + + // read the result. We need to read 2 lines + String method = mResultReader.readLine(); + String source = mResultReader.readLine(); + + // make the backtrace object and return it + if (method != null && source != null) { + return new NativeStackCallInfo(mLibrary, method, source); + } + } catch (IOException e) { + // log the error + Log.e("ddms", + "Error while trying to get information for addr: " + + tmp + " in library: " + mLibrary); + // we'll return null later + } + } + } + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java new file mode 100644 index 0000000..45d45ff --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2008 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 com.android.ddmlib.AllocationInfo; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Table; + +/** + * Base class for our information panels. + */ +public class AllocationPanel extends TablePanel { + + private final static String PREFS_ALLOC_COL_SIZE = "allocPanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_CLASS = "allocPanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_THREAD = "allocPanel.Col2"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_TRACE_CLASS = "allocPanel.Col3"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_TRACE_METHOD = "allocPanel.Col4"; //$NON-NLS-1$ + + private final static String PREFS_ALLOC_SASH = "allocPanel.sash"; //$NON-NLS-1$ + + private static final String PREFS_STACK_COL_CLASS = "allocPanel.stack.col0"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_METHOD = "allocPanel.stack.col1"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_FILE = "allocPanel.stack.col2"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_LINE = "allocPanel.stack.col3"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_NATIVE = "allocPanel.stack.col4"; //$NON-NLS-1$ + + private Composite mAllocationBase; + private Table mAllocationTable; + private TableViewer mAllocationViewer; + + private StackTracePanel mStackTracePanel; + private Table mStackTraceTable; + private Button mEnableButton; + private Button mRequestButton; + + /** + * Content Provider to display the allocations of a client. + * Expected input is a {@link Client} object, elements used in the table are of type + * {@link AllocationInfo}. + */ + private static class AllocationContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Client) { + AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations(); + if (allocs != null) { + return allocs; + } + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + /** + * A Label Provider to use with {@link AllocationContentProvider}. It expects the elements to be + * of type {@link AllocationInfo}. + */ + private static class AllocationLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof AllocationInfo) { + AllocationInfo alloc = (AllocationInfo)element; + switch (columnIndex) { + case 0: + return Integer.toString(alloc.getSize()); + case 1: + return alloc.getAllocatedClass(); + case 2: + return Short.toString(alloc.getThreadId()); + case 3: + StackTraceElement[] traces = alloc.getStackTrace(); + if (traces.length > 0) { + return traces[0].getClassName(); + } + break; + case 4: + traces = alloc.getStackTrace(); + if (traces.length > 0) { + return traces[0].getMethodName(); + } + break; + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + final IPreferenceStore store = DdmUiPreferences.getStore(); + + // base composite for selected client with enabled thread update. + mAllocationBase = new Composite(parent, SWT.NONE); + mAllocationBase.setLayout(new FormLayout()); + + // table above the sash + Composite topParent = new Composite(mAllocationBase, SWT.NONE); + topParent.setLayout(new GridLayout(2, false)); + + mEnableButton = new Button(topParent, SWT.PUSH); + mEnableButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Client current = getCurrentClient(); + int status = current.getClientData().getAllocationStatus(); + if (status == ClientData.ALLOCATION_TRACKING_ON) { + current.enableAllocationTracker(false); + } else { + current.enableAllocationTracker(true); + } + current.requestAllocationStatus(); + } + }); + + mRequestButton = new Button(topParent, SWT.PUSH); + mRequestButton.setText("Get Allocations"); + mRequestButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + getCurrentClient().requestAllocationDetails(); + } + }); + + setUpButtons(false /* enabled */, ClientData.ALLOCATION_TRACKING_OFF /* trackingStatus */); + + mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION); + GridData gridData; + mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH)); + gridData.horizontalSpan = 2; + mAllocationTable.setHeaderVisible(true); + mAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocation Size", + SWT.RIGHT, + "888", //$NON-NLS-1$ + PREFS_ALLOC_COL_SIZE, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated Class", + SWT.LEFT, + "Allocated Class", //$NON-NLS-1$ + PREFS_ALLOC_COL_CLASS, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Thread Id", + SWT.LEFT, + "999", //$NON-NLS-1$ + PREFS_ALLOC_COL_THREAD, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated in", + SWT.LEFT, + "utime", //$NON-NLS-1$ + PREFS_ALLOC_COL_TRACE_CLASS, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated in", + SWT.LEFT, + "utime", //$NON-NLS-1$ + PREFS_ALLOC_COL_TRACE_METHOD, store); + + mAllocationViewer = new TableViewer(mAllocationTable); + mAllocationViewer.setContentProvider(new AllocationContentProvider()); + mAllocationViewer.setLabelProvider(new AllocationLabelProvider()); + + mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + AllocationInfo selectedAlloc = getAllocationSelection(event.getSelection()); + updateAllocationStackTrace(selectedAlloc); + } + }); + + // the separating sash + final Sash sash = new Sash(mAllocationBase, SWT.HORIZONTAL); + Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + sash.setBackground(darkGray); + + // the UI below the sash + mStackTracePanel = new StackTracePanel(); + mStackTraceTable = mStackTracePanel.createPanel(mAllocationBase, + PREFS_STACK_COL_CLASS, + PREFS_STACK_COL_METHOD, + PREFS_STACK_COL_FILE, + PREFS_STACK_COL_LINE, + PREFS_STACK_COL_NATIVE, + store); + + // now setup the sash. + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + topParent.setLayoutData(data); + + final FormData sashData = new FormData(); + if (store != null && store.contains(PREFS_ALLOC_SASH)) { + sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mStackTraceTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = mAllocationBase.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + store.setValue(PREFS_ALLOC_SASH, e.y); + mAllocationBase.layout(); + } + } + }); + + return mAllocationBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mAllocationTable.setFocus(); + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) { + try { + mAllocationTable.getDisplay().asyncExec(new Runnable() { + public void run() { + mAllocationViewer.refresh(); + updateAllocationStackCall(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } else if ((changeMask & Client.CHANGE_HEAP_ALLOCATION_STATUS) != 0) { + try { + mAllocationTable.getDisplay().asyncExec(new Runnable() { + public void run() { + setUpButtons(true, client.getClientData().getAllocationStatus()); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mAllocationTable.isDisposed()) { + return; + } + + Client client = getCurrentClient(); + + mStackTracePanel.setCurrentClient(client); + mStackTracePanel.setViewerInput(null); // always empty on client selection change. + + if (client != null) { + setUpButtons(true /* enabled */, client.getClientData().getAllocationStatus()); + } else { + setUpButtons(false /* enabled */, + ClientData.ALLOCATION_TRACKING_OFF /* trackingStatus */); + } + + mAllocationViewer.setInput(client); + } + + /** + * Updates the stack call of the currently selected thread. + * <p/> + * This <b>must</b> be called from the UI thread. + */ + private void updateAllocationStackCall() { + Client client = getCurrentClient(); + if (client != null) { + // get the current selection in the ThreadTable + AllocationInfo selectedAlloc = getAllocationSelection(null); + + if (selectedAlloc != null) { + updateAllocationStackTrace(selectedAlloc); + } else { + updateAllocationStackTrace(null); + } + } + } + + /** + * updates the stackcall of the specified allocation. If <code>null</code> the UI is emptied + * of current data. + * @param thread + */ + private void updateAllocationStackTrace(AllocationInfo alloc) { + mStackTracePanel.setViewerInput(alloc); + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mAllocationTable); + addTableToFocusListener(mStackTraceTable); + } + + /** + * Returns the current allocation selection or <code>null</code> if none is found. + * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this + * selection is returned, otherwise, the <code>ISelection</code> returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or <code>null</code> + */ + private AllocationInfo getAllocationSelection(ISelection selection) { + if (selection == null) { + selection = mAllocationViewer.getSelection(); + } + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof AllocationInfo) { + return (AllocationInfo)object; + } + } + + return null; + } + + /** + * + * @param enabled + * @param trackingStatus + */ + private void setUpButtons(boolean enabled, int trackingStatus) { + if (enabled) { + switch (trackingStatus) { + case ClientData.ALLOCATION_TRACKING_UNKNOWN: + mEnableButton.setText("?"); + mEnableButton.setEnabled(false); + mRequestButton.setEnabled(false); + break; + case ClientData.ALLOCATION_TRACKING_OFF: + mEnableButton.setText("Start Tracking"); + mEnableButton.setEnabled(true); + mRequestButton.setEnabled(false); + break; + case ClientData.ALLOCATION_TRACKING_ON: + mEnableButton.setText("Stop Tracking"); + mEnableButton.setEnabled(true); + mRequestButton.setEnabled(true); + break; + } + } else { + mEnableButton.setEnabled(false); + mRequestButton.setEnabled(false); + mEnableButton.setText("Start Tracking"); + } + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java new file mode 100644 index 0000000..0ed4c95 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java @@ -0,0 +1,50 @@ +/* + * 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 com.android.ddmlib.Log; + +/** + * base background thread class. The class provides a synchronous quit method + * which sets a quitting flag to true. Inheriting classes should regularly test + * this flag with <code>isQuitting()</code> and should finish if the flag is + * true. + */ +public abstract class BackgroundThread extends Thread { + private boolean mQuit = false; + + /** + * Tell the thread to exit. This is usually called from the UI thread. The + * call is synchronous and will only return once the thread has terminated + * itself. + */ + public final void quit() { + mQuit = true; + Log.d("ddms", "Waiting for BackgroundThread to quit"); + try { + this.join(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + + /** returns if the thread was asked to quit. */ + protected final boolean isQuitting() { + return mQuit; + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java new file mode 100644 index 0000000..3e66ea5 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java @@ -0,0 +1,193 @@ +/* + * 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 com.android.ddmlib.HeapSegment; +import com.android.ddmlib.ClientData.HeapData; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + + +/** + * Base Panel for heap panels. + */ +public abstract class BaseHeapPanel extends TablePanel { + + /** store the processed heap segment, so that we don't recompute Image for nothing */ + protected byte[] mProcessedHeapData; + private Map<Integer, ArrayList<HeapSegmentElement>> mHeapMap; + + /** + * Serialize the heap data into an array. The resulting array is available through + * <code>getSerializedData()</code>. + * @param heapData The heap data to serialize + * @return true if the data changed. + */ + protected boolean serializeHeapData(HeapData heapData) { + Collection<HeapSegment> heapSegments; + + // Atomically get and clear the heap data. + synchronized (heapData) { + // get the segments + heapSegments = heapData.getHeapSegments(); + + + if (heapSegments != null) { + // if they are not null, we never processed them. + // Before we process then, we drop them from the HeapData + heapData.clearHeapData(); + + // process them into a linear byte[] + doSerializeHeapData(heapSegments); + heapData.setProcessedHeapData(mProcessedHeapData); + heapData.setProcessedHeapMap(mHeapMap); + + } else { + // the heap segments are null. Let see if the heapData contains a + // list that is already processed. + + byte[] pixData = heapData.getProcessedHeapData(); + + // and compare it to the one we currently have in the panel. + if (pixData == mProcessedHeapData) { + // looks like its the same + return false; + } else { + mProcessedHeapData = pixData; + } + + Map<Integer, ArrayList<HeapSegmentElement>> heapMap = + heapData.getProcessedHeapMap(); + mHeapMap = heapMap; + } + } + + return true; + } + + /** + * Returns the serialized heap data + */ + protected byte[] getSerializedData() { + return mProcessedHeapData; + } + + /** + * Processes and serialize the heapData. + * <p/> + * The resulting serialized array is {@link #mProcessedHeapData}. + * <p/> + * the resulting map is {@link #mHeapMap}. + * @param heapData the collection of {@link HeapSegment} that forms the heap data. + */ + private void doSerializeHeapData(Collection<HeapSegment> heapData) { + mHeapMap = new TreeMap<Integer, ArrayList<HeapSegmentElement>>(); + + Iterator<HeapSegment> iterator; + ByteArrayOutputStream out; + + out = new ByteArrayOutputStream(4 * 1024); + + iterator = heapData.iterator(); + while (iterator.hasNext()) { + HeapSegment hs = iterator.next(); + + HeapSegmentElement e = null; + while (true) { + int v; + + e = hs.getNextElement(null); + if (e == null) { + break; + } + + if (e.getSolidity() == HeapSegmentElement.SOLIDITY_FREE) { + v = 1; + } else { + v = e.getKind() + 2; + } + + // put the element in the map + ArrayList<HeapSegmentElement> elementList = mHeapMap.get(v); + if (elementList == null) { + elementList = new ArrayList<HeapSegmentElement>(); + mHeapMap.put(v, elementList); + } + elementList.add(e); + + + int len = e.getLength() / 8; + while (len > 0) { + out.write(v); + --len; + } + } + } + mProcessedHeapData = out.toByteArray(); + + // sort the segment element in the heap info. + Collection<ArrayList<HeapSegmentElement>> elementLists = mHeapMap.values(); + for (ArrayList<HeapSegmentElement> elementList : elementLists) { + Collections.sort(elementList); + } + } + + /** + * Creates a linear image of the heap data. + * @param pixData + * @param h + * @param palette + * @return + */ + protected ImageData createLinearHeapImage(byte[] pixData, int h, PaletteData palette) { + int w = pixData.length / h; + if (pixData.length % h != 0) { + w++; + } + + // Create the heap image. + ImageData id = new ImageData(w, h, 8, palette); + + int x = 0; + int y = 0; + for (byte b : pixData) { + if (b >= 0) { + id.setPixel(x, y, b); + } + + y++; + if (y >= h) { + y = 0; + x++; + } + } + + return id; + } + + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java new file mode 100644 index 0000000..a711933 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java @@ -0,0 +1,33 @@ +/* + * 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 com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +public abstract class ClientDisplayPanel extends SelectionDependentPanel + implements IClientChangeListener { + + @Override + protected void postCreation() { + AndroidDebugBridge.addClientChangeListener(this); + } + + public void dispose() { + AndroidDebugBridge.removeClientChangeListener(this); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java new file mode 100644 index 0000000..f832a4e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java @@ -0,0 +1,79 @@ +/* + * 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 org.eclipse.jface.preference.IPreferenceStore; + +/** + * Preference entry point for ddmuilib. Allows the lib to access a preference + * store (org.eclipse.jface.preference.IPreferenceStore) defined by the + * application that includes the lib. + */ +public final class DdmUiPreferences { + + public static final int DEFAULT_THREAD_REFRESH_INTERVAL = 4; // seconds + + private static int sThreadRefreshInterval = DEFAULT_THREAD_REFRESH_INTERVAL; + + private static IPreferenceStore mStore; + + private static String sSymbolLocation =""; //$NON-NLS-1$ + private static String sAddr2LineLocation =""; //$NON-NLS-1$ + private static String sTraceviewLocation =""; //$NON-NLS-1$ + + public static void setStore(IPreferenceStore store) { + mStore = store; + } + + public static IPreferenceStore getStore() { + return mStore; + } + + public static int getThreadRefreshInterval() { + return sThreadRefreshInterval; + } + + public static void setThreadRefreshInterval(int port) { + sThreadRefreshInterval = port; + } + + static String getSymbolDirectory() { + return sSymbolLocation; + } + + public static void setSymbolsLocation(String location) { + sSymbolLocation = location; + } + + static String getAddr2Line() { + return sAddr2LineLocation; + } + + public static void setAddr2LineLocation(String location) { + sAddr2LineLocation = location; + } + + public static String getTraceview() { + return sTraceviewLocation; + } + + public static void setTraceviewLocation(String location) { + sTraceviewLocation = location; + } + + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java new file mode 100644 index 0000000..81b757e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java @@ -0,0 +1,744 @@ +/* + * 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 com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.Device; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Device.DeviceState; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; + +import java.util.ArrayList; + +/** + * A display of both the devices and their clients. + */ +public final class DevicePanel extends Panel implements IDebugBridgeChangeListener, + IDeviceChangeListener, IClientChangeListener { + + private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$ + + private final static int DEVICE_COL_SERIAL = 0; + private final static int DEVICE_COL_STATE = 1; + // col 2, 3 not used. + private final static int DEVICE_COL_BUILD = 4; + + private final static int CLIENT_COL_NAME = 0; + private final static int CLIENT_COL_PID = 1; + private final static int CLIENT_COL_THREAD = 2; + private final static int CLIENT_COL_HEAP = 3; + private final static int CLIENT_COL_PORT = 4; + + public final static int ICON_WIDTH = 16; + public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$ + public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$ + public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$ + public final static String ICON_GC = "gc.png"; //$NON-NLS-1$ + + private Device mCurrentDevice; + private Client mCurrentClient; + + private Tree mTree; + private TreeViewer mTreeViewer; + + private Image mDeviceImage; + private Image mEmulatorImage; + + private Image mThreadImage; + private Image mHeapImage; + private Image mWaitingImage; + private Image mDebuggerImage; + private Image mDebugErrorImage; + + private final ArrayList<IUiSelectionListener> mListeners = new ArrayList<IUiSelectionListener>(); + + private final ArrayList<Device> mDevicesToExpand = new ArrayList<Device>(); + + private IImageLoader mLoader; + + private boolean mAdvancedPortSupport; + + /** + * A Content provider for the {@link TreeViewer}. + * <p/> + * The input is a {@link AndroidDebugBridge}. First level elements are {@link Device} objects, + * and second level elements are {@link Client} object. + */ + private class ContentProvider implements ITreeContentProvider { + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof Device) { + return ((Device)parentElement).getClients(); + } + return new Object[0]; + } + + public Object getParent(Object element) { + if (element instanceof Client) { + return ((Client)element).getDevice(); + } + return null; + } + + public boolean hasChildren(Object element) { + if (element instanceof Device) { + return ((Device)element).hasClients(); + } + + // Clients never have children. + return false; + } + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof AndroidDebugBridge) { + return ((AndroidDebugBridge)inputElement).getDevices(); + } + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + /** + * A Label Provider for the {@link TreeViewer} in {@link DevicePanel}. It provides + * labels and images for {@link Device} and {@link Client} objects. + */ + private class LabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + if (columnIndex == DEVICE_COL_SERIAL && element instanceof Device) { + Device device = (Device)element; + if (device.isEmulator()) { + return mEmulatorImage; + } + + return mDeviceImage; + } else if (element instanceof Client) { + Client client = (Client)element; + ClientData cd = client.getClientData(); + + switch (columnIndex) { + case CLIENT_COL_NAME: + switch (cd.getDebuggerConnectionStatus()) { + case ClientData.DEBUGGER_DEFAULT: + return null; + case ClientData.DEBUGGER_WAITING: + return mWaitingImage; + case ClientData.DEBUGGER_ATTACHED: + return mDebuggerImage; + case ClientData.DEBUGGER_ERROR: + return mDebugErrorImage; + } + return null; + case CLIENT_COL_THREAD: + if (client.isThreadUpdateEnabled()) { + return mThreadImage; + } + return null; + case CLIENT_COL_HEAP: + if (client.isHeapUpdateEnabled()) { + return mHeapImage; + } + return null; + } + } + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof Device) { + Device device = (Device)element; + switch (columnIndex) { + case DEVICE_COL_SERIAL: + return device.getSerialNumber(); + case DEVICE_COL_STATE: + return getStateString(device); + case DEVICE_COL_BUILD: { + String version = device.getProperty(Device.PROP_BUILD_VERSION); + if (version != null) { + String debuggable = device.getProperty(Device.PROP_DEBUGGABLE); + if (device.isEmulator()) { + String avdName = device.getAvdName(); + if (avdName == null) { + avdName = "?"; // the device is probably not online yet, so + // we don't know its AVD name just yet. + } + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return String.format("%1$s [%2$s, debug]", avdName, + version); + } else { + return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$ + } + } else { + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return String.format("%1$s, debug", version); + } else { + return String.format("%1$s", version); //$NON-NLS-1$ + } + } + } else { + return "unknown"; + } + } + } + } else if (element instanceof Client) { + Client client = (Client)element; + ClientData cd = client.getClientData(); + + switch (columnIndex) { + case CLIENT_COL_NAME: + String name = cd.getClientDescription(); + if (name != null) { + return name; + } + return "?"; + case CLIENT_COL_PID: + return Integer.toString(cd.getPid()); + case CLIENT_COL_PORT: + if (mAdvancedPortSupport) { + int port = client.getDebuggerListenPort(); + String portString = "?"; + if (port != 0) { + portString = Integer.toString(port); + } + if (client.isSelectedClient()) { + return String.format("%1$s / %2$d", portString, //$NON-NLS-1$ + DdmPreferences.getSelectedDebugPort()); + } + + return portString; + } + } + } + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Classes which implement this interface provide methods that deals + * with {@link Device} and {@link Client} selection changes coming from the ui. + */ + public interface IUiSelectionListener { + /** + * Sent when a new {@link Device} and {@link Client} are selected. + * @param selectedDevice the selected device. If null, no devices are selected. + * @param selectedClient The selected client. If null, no clients are selected. + */ + public void selectionChanged(Device selectedDevice, Client selectedClient); + } + + /** + * Creates the {@link DevicePanel} object. + * @param loader + * @param advancedPortSupport if true the device panel will add support for selected client port + * and display the ports in the ui. + */ + public DevicePanel(IImageLoader loader, boolean advancedPortSupport) { + mLoader = loader; + mAdvancedPortSupport = advancedPortSupport; + } + + public void addSelectionListener(IUiSelectionListener listener) { + mListeners.add(listener); + } + + public void removeSelectionListener(IUiSelectionListener listener) { + mListeners.remove(listener); + } + + @Override + protected Control createControl(Composite parent) { + loadImages(parent.getDisplay(), mLoader); + + parent.setLayout(new FillLayout()); + + // create the tree and its column + mTree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION); + mTree.setHeaderVisible(true); + mTree.setLinesVisible(true); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, + "com.android.home", //$NON-NLS-1$ + PREFS_COL_NAME_SERIAL, store); + TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$ + "Offline", //$NON-NLS-1$ + PREFS_COL_PID_STATE, store); + + TreeColumn col = new TreeColumn(mTree, SWT.NONE); + col.setWidth(ICON_WIDTH + 8); + col.setResizable(false); + col = new TreeColumn(mTree, SWT.NONE); + col.setWidth(ICON_WIDTH + 8); + col.setResizable(false); + + TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$ + "9999-9999", //$NON-NLS-1$ + PREFS_COL_PORT_BUILD, store); + + // create the tree viewer + mTreeViewer = new TreeViewer(mTree); + + // make the device auto expanded. + mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); + + // set up the content and label providers. + mTreeViewer.setContentProvider(new ContentProvider()); + mTreeViewer.setLabelProvider(new LabelProvider()); + + mTree.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + notifyListeners(); + } + }); + + return mTree; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTree.setFocus(); + } + + @Override + protected void postCreation() { + // ask for notification of changes in AndroidDebugBridge (a new one is created when + // adb is restarted from a different location), Device and Client objects. + AndroidDebugBridge.addDebugBridgeChangeListener(this); + AndroidDebugBridge.addDeviceChangeListener(this); + AndroidDebugBridge.addClientChangeListener(this); + } + + public void dispose() { + AndroidDebugBridge.removeDebugBridgeChangeListener(this); + AndroidDebugBridge.removeDeviceChangeListener(this); + AndroidDebugBridge.removeClientChangeListener(this); + } + + /** + * Returns the selected {@link Client}. May be null. + */ + public Client getSelectedClient() { + return mCurrentClient; + } + + /** + * Returns the selected {@link Device}. If a {@link Client} is selected, it returns the + * Device object containing the client. + */ + public Device getSelectedDevice() { + return mCurrentDevice; + } + + /** + * Kills the selected {@link Client} by sending its VM a halt command. + */ + public void killSelectedClient() { + if (mCurrentClient != null) { + Client client = mCurrentClient; + + // reset the selection to the device. + TreePath treePath = new TreePath(new Object[] { mCurrentDevice }); + TreeSelection treeSelection = new TreeSelection(treePath); + mTreeViewer.setSelection(treeSelection); + + client.kill(); + } + } + + /** + * Forces a GC on the selected {@link Client}. + */ + public void forceGcOnSelectedClient() { + if (mCurrentClient != null) { + mCurrentClient.executeGarbageCollector(); + } + } + + public void setEnabledHeapOnSelectedClient(boolean enable) { + if (mCurrentClient != null) { + mCurrentClient.setHeapUpdateEnabled(enable); + } + } + + public void setEnabledThreadOnSelectedClient(boolean enable) { + if (mCurrentClient != null) { + mCurrentClient.setThreadUpdateEnabled(enable); + } + } + + /** + * Sent when a new {@link AndroidDebugBridge} is started. + * <p/> + * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + * + * @see IDebugBridgeChangeListener#serverChanged(AndroidDebugBridge) + */ + public void bridgeChanged(final AndroidDebugBridge bridge) { + if (mTree.isDisposed() == false) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // set up the data source. + mTreeViewer.setInput(bridge); + + // notify the listener of a possible selection change. + notifyListeners(); + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + // all current devices are obsolete + synchronized (mDevicesToExpand) { + mDevicesToExpand.clear(); + } + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceConnected(Device) + */ + public void deviceConnected(Device device) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // refresh all + mTreeViewer.refresh(); + + // notify the listener of a possible selection change. + notifyListeners(); + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + + // if it doesn't have clients yet, it'll need to be manually expanded when it gets them. + if (device.hasClients() == false) { + synchronized (mDevicesToExpand) { + mDevicesToExpand.add(device); + } + } + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(Device) + */ + public void deviceDisconnected(Device device) { + deviceConnected(device); + + // just in case, we remove it from the list of devices to expand. + synchronized (mDevicesToExpand) { + mDevicesToExpand.remove(device); + } + } + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + * <p/> + * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask indicating what changed. + * + * @see IDeviceChangeListener#deviceChanged(Device) + */ + public void deviceChanged(final Device device, int changeMask) { + boolean expand = false; + synchronized (mDevicesToExpand) { + int index = mDevicesToExpand.indexOf(device); + if (device.hasClients() && index != -1) { + mDevicesToExpand.remove(index); + expand = true; + } + } + + final boolean finalExpand = expand; + + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // look if the current device is selected. This is done in case the current + // client of this particular device was killed. In this case, we'll need to + // manually reselect the device. + + Device selectedDevice = getSelectedDevice(); + + // refresh the device + mTreeViewer.refresh(device); + + // if the selected device was the changed device and the new selection is + // empty, we reselect the device. + if (selectedDevice == device && mTreeViewer.getSelection().isEmpty()) { + mTreeViewer.setSelection(new TreeSelection(new TreePath( + new Object[] { device }))); + } + + // notify the listener of a possible selection change. + notifyListeners(); + + if (finalExpand) { + mTreeViewer.setExpandedState(device, true); + } + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, final int changeMask) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // refresh the client + mTreeViewer.refresh(client); + + if ((changeMask & Client.CHANGE_DEBUGGER_INTEREST) == + Client.CHANGE_DEBUGGER_INTEREST && + client.getClientData().getDebuggerConnectionStatus() == + ClientData.DEBUGGER_WAITING) { + // make sure the device is expanded. Normally the setSelection below + // will auto expand, but the children of device may not already exist + // at this time. Forcing an expand will make the TreeViewer create them. + Device device = client.getDevice(); + if (mTreeViewer.getExpandedState(device) == false) { + mTreeViewer.setExpandedState(device, true); + } + + // create and set the selection + TreePath treePath = new TreePath(new Object[] { device, client}); + TreeSelection treeSelection = new TreeSelection(treePath); + mTreeViewer.setSelection(treeSelection); + + if (mAdvancedPortSupport) { + client.setAsSelectedClient(); + } + + // notify the listener of a possible selection change. + notifyListeners(device, client); + } + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + private void loadImages(Display display, IImageLoader loader) { + if (mDeviceImage == null) { + mDeviceImage = ImageHelper.loadImage(loader, display, "device.png", //$NON-NLS-1$ + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } + if (mEmulatorImage == null) { + mEmulatorImage = ImageHelper.loadImage(loader, display, + "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_BLUE)); + } + if (mThreadImage == null) { + mThreadImage = ImageHelper.loadImage(loader, display, ICON_THREAD, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_YELLOW)); + } + if (mHeapImage == null) { + mHeapImage = ImageHelper.loadImage(loader, display, ICON_HEAP, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_BLUE)); + } + if (mWaitingImage == null) { + mWaitingImage = ImageHelper.loadImage(loader, display, + "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_RED)); + } + if (mDebuggerImage == null) { + mDebuggerImage = ImageHelper.loadImage(loader, display, + "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_GREEN)); + } + if (mDebugErrorImage == null) { + mDebugErrorImage = ImageHelper.loadImage(loader, display, + "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_RED)); + } + } + + /** + * Returns a display string representing the state of the device. + * @param d the device + */ + private static String getStateString(Device d) { + DeviceState deviceState = d.getState(); + if (deviceState == DeviceState.ONLINE) { + return "Online"; + } else if (deviceState == DeviceState.OFFLINE) { + return "Offline"; + } else if (deviceState == DeviceState.BOOTLOADER) { + return "Bootloader"; + } + + return "??"; + } + + /** + * Executes the {@link Runnable} in the UI thread. + * @param runnable the runnable to execute. + */ + private void exec(Runnable runnable) { + try { + Display display = mTree.getDisplay(); + display.asyncExec(runnable); + } catch (SWTException e) { + // tree is disposed, we need to do something. lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(this); + AndroidDebugBridge.removeDeviceChangeListener(this); + AndroidDebugBridge.removeClientChangeListener(this); + } + } + + private void notifyListeners() { + // get the selection + TreeItem[] items = mTree.getSelection(); + + Client client = null; + Device device = null; + + if (items.length == 1) { + Object object = items[0].getData(); + if (object instanceof Client) { + client = (Client)object; + device = client.getDevice(); + } else if (object instanceof Device) { + device = (Device)object; + } + } + + notifyListeners(device, client); + } + + private void notifyListeners(Device selectedDevice, Client selectedClient) { + if (selectedDevice != mCurrentDevice || selectedClient != mCurrentClient) { + mCurrentDevice = selectedDevice; + mCurrentClient = selectedClient; + + for (IUiSelectionListener listener : mListeners) { + // notify the listener with a try/catch-all to make sure this thread won't die + // because of an uncaught exception before all the listeners were notified. + try { + listener.selectionChanged(selectedDevice, selectedClient); + } catch (Exception e) { + } + } + } + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java new file mode 100644 index 0000000..5583760 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java @@ -0,0 +1,1454 @@ +/* + * 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 com.android.ddmlib.Device; +import com.android.ddmlib.EmulatorConsole; +import com.android.ddmlib.EmulatorConsole.GsmMode; +import com.android.ddmlib.EmulatorConsole.GsmStatus; +import com.android.ddmlib.EmulatorConsole.NetworkStatus; +import com.android.ddmuilib.location.CoordinateControls; +import com.android.ddmuilib.location.GpxParser; +import com.android.ddmuilib.location.KmlParser; +import com.android.ddmuilib.location.TrackContentProvider; +import com.android.ddmuilib.location.TrackLabelProvider; +import com.android.ddmuilib.location.TrackPoint; +import com.android.ddmuilib.location.WayPoint; +import com.android.ddmuilib.location.WayPointContentProvider; +import com.android.ddmuilib.location.WayPointLabelProvider; +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.Text; + +/** + * Panel to control the emulator using EmulatorConsole objects. + */ +public class EmulatorControlPanel extends SelectionDependentPanel { + + // default location: Patio outside Charlie's + private final static double DEFAULT_LONGITUDE = -122.084095; + private final static double DEFAULT_LATITUDE = 37.422006; + + private final static String SPEED_FORMAT = "Speed: %1$dX"; + + + /** + * Map between the display gsm mode and the internal tag used by the display. + */ + private final static String[][] GSM_MODES = new String[][] { + { "unregistered", GsmMode.UNREGISTERED.getTag() }, + { "home", GsmMode.HOME.getTag() }, + { "roaming", GsmMode.ROAMING.getTag() }, + { "searching", GsmMode.SEARCHING.getTag() }, + { "denied", GsmMode.DENIED.getTag() }, + }; + + private final static String[] NETWORK_SPEEDS = new String[] { + "Full", + "GSM", + "HSCSD", + "GPRS", + "EDGE", + "UMTS", + "HSDPA", + }; + + private final static String[] NETWORK_LATENCIES = new String[] { + "None", + "GPRS", + "EDGE", + "UMTS", + }; + + private final static int[] PLAY_SPEEDS = new int[] { 1, 2, 5, 10, 20, 50 }; + + private final static String RE_PHONE_NUMBER = "^[+#0-9]+$"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_NAME = "emulatorControl.waypoint.name"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_LONGITUDE = "emulatorControl.waypoint.longitude"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_LATITUDE = "emulatorControl.waypoint.latitude"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_ELEVATION = "emulatorControl.waypoint.elevation"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_DESCRIPTION = "emulatorControl.waypoint.desc"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_NAME = "emulatorControl.track.name"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_COUNT = "emulatorControl.track.count"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_FIRST = "emulatorControl.track.first"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_LAST = "emulatorControl.track.last"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_COMMENT = "emulatorControl.track.comment"; //$NON-NLS-1$ + + private IImageLoader mImageLoader; + + private EmulatorConsole mEmulatorConsole; + + private Composite mParent; + + private Label mVoiceLabel; + private Combo mVoiceMode; + private Label mDataLabel; + private Combo mDataMode; + private Label mSpeedLabel; + private Combo mNetworkSpeed; + private Label mLatencyLabel; + private Combo mNetworkLatency; + + private Label mNumberLabel; + private Text mPhoneNumber; + + private Button mVoiceButton; + private Button mSmsButton; + + private Label mMessageLabel; + private Text mSmsMessage; + + private Button mCallButton; + private Button mCancelButton; + + private TabFolder mLocationFolders; + + private Button mDecimalButton; + private Button mSexagesimalButton; + private CoordinateControls mLongitudeControls; + private CoordinateControls mLatitudeControls; + private Button mGpxUploadButton; + private Table mGpxWayPointTable; + private Table mGpxTrackTable; + private Button mKmlUploadButton; + private Table mKmlWayPointTable; + + private Button mPlayGpxButton; + private Button mGpxBackwardButton; + private Button mGpxForwardButton; + private Button mGpxSpeedButton; + private Button mPlayKmlButton; + private Button mKmlBackwardButton; + private Button mKmlForwardButton; + private Button mKmlSpeedButton; + + private Image mPlayImage; + private Image mPauseImage; + + private Thread mPlayingThread; + private boolean mPlayingTrack; + private int mPlayDirection = 1; + private int mSpeed; + private int mSpeedIndex; + + private final SelectionAdapter mDirectionButtonAdapter = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Button b = (Button)e.getSource(); + if (b.getSelection() == false) { + // basically the button was unselected, which we don't allow. + // so we reselect it. + b.setSelection(true); + return; + } + + // now handle selection change. + if (b == mGpxForwardButton || b == mKmlForwardButton) { + mGpxBackwardButton.setSelection(false); + mGpxForwardButton.setSelection(true); + mKmlBackwardButton.setSelection(false); + mKmlForwardButton.setSelection(true); + mPlayDirection = 1; + + } else { + mGpxBackwardButton.setSelection(true); + mGpxForwardButton.setSelection(false); + mKmlBackwardButton.setSelection(true); + mKmlForwardButton.setSelection(false); + mPlayDirection = -1; + } + } + }; + + private final SelectionAdapter mSpeedButtonAdapter = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mSpeedIndex = (mSpeedIndex+1) % PLAY_SPEEDS.length; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mGpxPlayControls.pack(); + mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mKmlPlayControls.pack(); + + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + }; + private Composite mKmlPlayControls; + private Composite mGpxPlayControls; + + + public EmulatorControlPanel(IImageLoader imageLoader) { + mImageLoader = imageLoader; + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + handleNewDevice(getCurrentDevice()); + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()} + */ + @Override + public void clientSelected() { + // pass + } + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + + final ScrolledComposite scollingParent = new ScrolledComposite(parent, SWT.V_SCROLL); + scollingParent.setExpandVertical(true); + scollingParent.setExpandHorizontal(true); + scollingParent.setLayoutData(new GridData(GridData.FILL_BOTH)); + + final Composite top = new Composite(scollingParent, SWT.NONE); + scollingParent.setContent(top); + top.setLayout(new GridLayout(1, false)); + + // set the resize for the scrolling to work (why isn't that done automatically?!?) + scollingParent.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = scollingParent.getClientArea(); + scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT)); + } + }); + + createRadioControls(top); + + createCallControls(top); + + createLocationControls(top); + + doEnable(false); + + top.layout(); + Rectangle r = scollingParent.getClientArea(); + scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT)); + + return scollingParent; + } + + /** + * Create Radio (on/off/roaming, for voice/data) controls. + * @param top + */ + private void createRadioControls(final Composite top) { + Group g1 = new Group(top, SWT.NONE); + g1.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + g1.setLayout(new GridLayout(2, false)); + g1.setText("Telephony Status"); + + // the inside of the group is 2 composite so that all the column of the controls (mainly + // combos) have the same width, while not taking the whole screen width + Composite insideGroup = new Composite(g1, SWT.NONE); + GridLayout gl = new GridLayout(4, false); + gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0; + insideGroup.setLayout(gl); + + mVoiceLabel = new Label(insideGroup, SWT.NONE); + mVoiceLabel.setText("Voice:"); + mVoiceLabel.setAlignment(SWT.RIGHT); + + mVoiceMode = new Combo(insideGroup, SWT.READ_ONLY); + mVoiceMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String[] mode : GSM_MODES) { + mVoiceMode.add(mode[0]); + } + mVoiceMode.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setVoiceMode(mVoiceMode.getSelectionIndex()); + } + }); + + mSpeedLabel = new Label(insideGroup, SWT.NONE); + mSpeedLabel.setText("Speed:"); + mSpeedLabel.setAlignment(SWT.RIGHT); + + mNetworkSpeed = new Combo(insideGroup, SWT.READ_ONLY); + mNetworkSpeed.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String mode : NETWORK_SPEEDS) { + mNetworkSpeed.add(mode); + } + mNetworkSpeed.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setNetworkSpeed(mNetworkSpeed.getSelectionIndex()); + } + }); + + mDataLabel = new Label(insideGroup, SWT.NONE); + mDataLabel.setText("Data:"); + mDataLabel.setAlignment(SWT.RIGHT); + + mDataMode = new Combo(insideGroup, SWT.READ_ONLY); + mDataMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String[] mode : GSM_MODES) { + mDataMode.add(mode[0]); + } + mDataMode.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setDataMode(mDataMode.getSelectionIndex()); + } + }); + + mLatencyLabel = new Label(insideGroup, SWT.NONE); + mLatencyLabel.setText("Latency:"); + mLatencyLabel.setAlignment(SWT.RIGHT); + + mNetworkLatency = new Combo(insideGroup, SWT.READ_ONLY); + mNetworkLatency.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String mode : NETWORK_LATENCIES) { + mNetworkLatency.add(mode); + } + mNetworkLatency.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setNetworkLatency(mNetworkLatency.getSelectionIndex()); + } + }); + + // now an empty label to take the rest of the width of the group + Label l = new Label(g1, SWT.NONE); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + } + + /** + * Create Voice/SMS call/hang up controls + * @param top + */ + private void createCallControls(final Composite top) { + GridLayout gl; + Group g2 = new Group(top, SWT.NONE); + g2.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + g2.setLayout(new GridLayout(1, false)); + g2.setText("Telephony Actions"); + + // horizontal composite for label + text field + Composite phoneComp = new Composite(g2, SWT.NONE); + phoneComp.setLayoutData(new GridData(GridData.FILL_BOTH)); + gl = new GridLayout(2, false); + gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0; + phoneComp.setLayout(gl); + + mNumberLabel = new Label(phoneComp, SWT.NONE); + mNumberLabel.setText("Incoming number:"); + + mPhoneNumber = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + mPhoneNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mPhoneNumber.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + // Reenable the widgets based on the content of the text. + // doEnable checks the validity of the phone number to enable/disable some + // widgets. + // Looks like we're getting a callback at creation time, so we can't + // suppose that we are enabled when the text is modified... + doEnable(mEmulatorConsole != null); + } + }); + + mVoiceButton = new Button(phoneComp, SWT.RADIO); + GridData gd = new GridData(); + gd.horizontalSpan = 2; + mVoiceButton.setText("Voice"); + mVoiceButton.setLayoutData(gd); + mVoiceButton.setEnabled(false); + mVoiceButton.setSelection(true); + mVoiceButton.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + doEnable(true); + + if (mVoiceButton.getSelection()) { + mCallButton.setText("Call"); + } else { + mCallButton.setText("Send"); + } + } + }); + + mSmsButton = new Button(phoneComp, SWT.RADIO); + mSmsButton.setText("SMS"); + gd = new GridData(); + gd.horizontalSpan = 2; + mSmsButton.setLayoutData(gd); + mSmsButton.setEnabled(false); + // Since there are only 2 radio buttons, we can put a listener on only one (they + // are both called on select and unselect event. + + mMessageLabel = new Label(phoneComp, SWT.NONE); + gd = new GridData(); + gd.verticalAlignment = SWT.TOP; + mMessageLabel.setLayoutData(gd); + mMessageLabel.setText("Message:"); + mMessageLabel.setEnabled(false); + + mSmsMessage = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL); + mSmsMessage.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 70; + mSmsMessage.setEnabled(false); + + // composite to put the 2 buttons horizontally + Composite g2ButtonComp = new Composite(g2, SWT.NONE); + g2ButtonComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + gl = new GridLayout(2, false); + gl.marginWidth = gl.marginHeight = 0; + g2ButtonComp.setLayout(gl); + + // now a button below the phone number + mCallButton = new Button(g2ButtonComp, SWT.PUSH); + mCallButton.setText("Call"); + mCallButton.setEnabled(false); + mCallButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + if (mVoiceButton.getSelection()) { + processCommandResult(mEmulatorConsole.call(mPhoneNumber.getText().trim())); + } else { + // we need to encode the message. We need to replace the carriage return + // character by the 2 character string \n. + // Because of this the \ character needs to be escaped as well. + // ReplaceAll() expects regexp so \ char are escaped twice. + String message = mSmsMessage.getText(); + message = message.replaceAll("\\\\", //$NON-NLS-1$ + "\\\\\\\\"); //$NON-NLS-1$ + + // While the normal line delimiter is returned by Text.getLineDelimiter() + // it seems copy pasting text coming from somewhere else could have another + // delimited. For this reason, we'll replace is several steps + + // replace the dual CR-LF + message = message.replaceAll("\r\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + // replace remaining stand alone \n + message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + // replace remaining stand alone \r + message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + processCommandResult(mEmulatorConsole.sendSms(mPhoneNumber.getText().trim(), + message)); + } + } + } + }); + + mCancelButton = new Button(g2ButtonComp, SWT.PUSH); + mCancelButton.setText("Hang Up"); + mCancelButton.setEnabled(false); + mCancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + if (mVoiceButton.getSelection()) { + processCommandResult(mEmulatorConsole.cancelCall( + mPhoneNumber.getText().trim())); + } + } + } + }); + } + + /** + * Create Location controls. + * @param top + */ + private void createLocationControls(final Composite top) { + Label l = new Label(top, SWT.NONE); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + l.setText("Location Controls"); + + mLocationFolders = new TabFolder(top, SWT.NONE); + mLocationFolders.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Composite manualLocationComp = new Composite(mLocationFolders, SWT.NONE); + TabItem item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("Manual"); + item.setControl(manualLocationComp); + + createManualLocationControl(manualLocationComp); + + mPlayImage = mImageLoader.loadImage("play.png", mParent.getDisplay()); // $NON-NLS-1$ + mPauseImage = mImageLoader.loadImage("pause.png", mParent.getDisplay()); // $NON-NLS-1$ + + Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE); + item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("GPX"); + item.setControl(gpxLocationComp); + + createGpxLocationControl(gpxLocationComp); + + Composite kmlLocationComp = new Composite(mLocationFolders, SWT.NONE); + kmlLocationComp.setLayout(new FillLayout()); + item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("KML"); + item.setControl(kmlLocationComp); + + createKmlLocationControl(kmlLocationComp); + } + + private void createManualLocationControl(Composite manualLocationComp) { + final StackLayout sl; + GridLayout gl; + Label label; + + manualLocationComp.setLayout(new GridLayout(1, false)); + mDecimalButton = new Button(manualLocationComp, SWT.RADIO); + mDecimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDecimalButton.setText("Decimal"); + mSexagesimalButton = new Button(manualLocationComp, SWT.RADIO); + mSexagesimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSexagesimalButton.setText("Sexagesimal"); + + // composite to hold and switching between the 2 modes. + final Composite content = new Composite(manualLocationComp, SWT.NONE); + content.setLayout(sl = new StackLayout()); + + // decimal display + final Composite decimalContent = new Composite(content, SWT.NONE); + decimalContent.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mLongitudeControls = new CoordinateControls(); + mLatitudeControls = new CoordinateControls(); + + label = new Label(decimalContent, SWT.NONE); + label.setText("Longitude"); + + mLongitudeControls.createDecimalText(decimalContent); + + label = new Label(decimalContent, SWT.NONE); + label.setText("Latitude"); + + mLatitudeControls.createDecimalText(decimalContent); + + // sexagesimal content + final Composite sexagesimalContent = new Composite(content, SWT.NONE); + sexagesimalContent.setLayout(gl = new GridLayout(7, false)); + gl.marginHeight = gl.marginWidth = 0; + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("Longitude"); + + mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\u00B0"); // degree character + + mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("'"); + + mLongitudeControls.createSexagesimalSecondText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\""); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("Latitude"); + + mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\u00B0"); + + mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("'"); + + mLatitudeControls.createSexagesimalSecondText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\""); + + // set the default display to decimal + sl.topControl = decimalContent; + mDecimalButton.setSelection(true); + + mDecimalButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mDecimalButton.getSelection()) { + sl.topControl = decimalContent; + } else { + sl.topControl = sexagesimalContent; + } + content.layout(); + } + }); + + Button sendButton = new Button(manualLocationComp, SWT.PUSH); + sendButton.setText("Send"); + sendButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.sendLocation( + mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0)); + } + } + }); + + mLongitudeControls.setValue(DEFAULT_LONGITUDE); + mLatitudeControls.setValue(DEFAULT_LATITUDE); + } + + private void createGpxLocationControl(Composite gpxLocationComp) { + GridData gd; + + IPreferenceStore store = DdmUiPreferences.getStore(); + + gpxLocationComp.setLayout(new GridLayout(1, false)); + + mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH); + mGpxUploadButton.setText("Load GPX..."); + + // Table for way point + mGpxWayPointTable = new Table(gpxLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 100; + mGpxWayPointTable.setHeaderVisible(true); + mGpxWayPointTable.setLinesVisible(true); + + TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT, + "Some Name", + PREFS_WAYPOINT_COL_NAME, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LONGITUDE, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LATITUDE, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT, + "99999.9", + PREFS_WAYPOINT_COL_ELEVATION, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT, + "Some Description", + PREFS_WAYPOINT_COL_DESCRIPTION, store); + + final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable); + gpxWayPointViewer.setContentProvider(new WayPointContentProvider()); + gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider()); + + gpxWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + processCommandResult(mEmulatorConsole.sendLocation( + wayPoint.getLongitude(), wayPoint.getLatitude(), + wayPoint.getElevation())); + } + } + } + } + }); + + // table for tracks. + mGpxTrackTable = new Table(gpxLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mGpxTrackTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 100; + mGpxTrackTable.setHeaderVisible(true); + mGpxTrackTable.setLinesVisible(true); + + TableHelper.createTableColumn(mGpxTrackTable, "Name", SWT.LEFT, + "Some very long name", + PREFS_TRACK_COL_NAME, store); + TableHelper.createTableColumn(mGpxTrackTable, "Point Count", SWT.RIGHT, + "9999", + PREFS_TRACK_COL_COUNT, store); + TableHelper.createTableColumn(mGpxTrackTable, "First Point Time", SWT.LEFT, + "999-99-99T99:99:99Z", + PREFS_TRACK_COL_FIRST, store); + TableHelper.createTableColumn(mGpxTrackTable, "Last Point Time", SWT.LEFT, + "999-99-99T99:99:99Z", + PREFS_TRACK_COL_LAST, store); + TableHelper.createTableColumn(mGpxTrackTable, "Comment", SWT.LEFT, + "-199.999999", + PREFS_TRACK_COL_COMMENT, store); + + final TableViewer gpxTrackViewer = new TableViewer(mGpxTrackTable); + gpxTrackViewer.setContentProvider(new TrackContentProvider()); + gpxTrackViewer.setLabelProvider(new TrackLabelProvider()); + + gpxTrackViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof Track) { + Track track = (Track)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + TrackPoint[] points = track.getPoints(); + processCommandResult(mEmulatorConsole.sendLocation( + points[0].getLongitude(), points[0].getLatitude(), + points[0].getElevation())); + } + + mPlayGpxButton.setEnabled(true); + mGpxBackwardButton.setEnabled(true); + mGpxForwardButton.setEnabled(true); + mGpxSpeedButton.setEnabled(true); + + return; + } + } + + mPlayGpxButton.setEnabled(false); + mGpxBackwardButton.setEnabled(false); + mGpxForwardButton.setEnabled(false); + mGpxSpeedButton.setEnabled(false); + } + }); + + mGpxUploadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Load GPX File"); + fileDialog.setFilterExtensions(new String[] { "*.gpx" } ); + + String fileName = fileDialog.open(); + if (fileName != null) { + GpxParser parser = new GpxParser(fileName); + if (parser.parse()) { + gpxWayPointViewer.setInput(parser.getWayPoints()); + gpxTrackViewer.setInput(parser.getTracks()); + } + } + } + }); + + mGpxPlayControls = new Composite(gpxLocationComp, SWT.NONE); + GridLayout gl; + mGpxPlayControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + mGpxPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mPlayGpxButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT); + mPlayGpxButton.setImage(mPlayImage); + mPlayGpxButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mPlayingTrack == false) { + ISelection selection = gpxTrackViewer.getSelection(); + if (selection.isEmpty() == false && selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof Track) { + Track track = (Track)selectedObject; + playTrack(track); + } + } + } else { + // if we're playing, then we pause + mPlayingTrack = false; + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + } + }); + + Label separator = new Label(mGpxPlayControls, SWT.SEPARATOR | SWT.VERTICAL); + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + + mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$ + mGpxBackwardButton.setSelection(false); + mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxForwardButton.setImage(mImageLoader.loadImage("forward.png", mParent.getDisplay())); // $NON-NLS-1$ + mGpxForwardButton.setSelection(true); + mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter); + + mGpxSpeedButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT); + + mSpeedIndex = 0; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mGpxSpeedButton.addSelectionListener(mSpeedButtonAdapter); + + mPlayGpxButton.setEnabled(false); + mGpxBackwardButton.setEnabled(false); + mGpxForwardButton.setEnabled(false); + mGpxSpeedButton.setEnabled(false); + + } + + private void createKmlLocationControl(Composite kmlLocationComp) { + GridData gd; + + IPreferenceStore store = DdmUiPreferences.getStore(); + + kmlLocationComp.setLayout(new GridLayout(1, false)); + + mKmlUploadButton = new Button(kmlLocationComp, SWT.PUSH); + mKmlUploadButton.setText("Load KML..."); + + // Table for way point + mKmlWayPointTable = new Table(kmlLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mKmlWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 200; + mKmlWayPointTable.setHeaderVisible(true); + mKmlWayPointTable.setLinesVisible(true); + + TableHelper.createTableColumn(mKmlWayPointTable, "Name", SWT.LEFT, + "Some Name", + PREFS_WAYPOINT_COL_NAME, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Longitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LONGITUDE, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Latitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LATITUDE, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Elevation", SWT.LEFT, + "99999.9", + PREFS_WAYPOINT_COL_ELEVATION, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Description", SWT.LEFT, + "Some Description", + PREFS_WAYPOINT_COL_DESCRIPTION, store); + + final TableViewer kmlWayPointViewer = new TableViewer(mKmlWayPointTable); + kmlWayPointViewer.setContentProvider(new WayPointContentProvider()); + kmlWayPointViewer.setLabelProvider(new WayPointLabelProvider()); + + mKmlUploadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Load KML File"); + fileDialog.setFilterExtensions(new String[] { "*.kml" } ); + + String fileName = fileDialog.open(); + if (fileName != null) { + KmlParser parser = new KmlParser(fileName); + if (parser.parse()) { + kmlWayPointViewer.setInput(parser.getWayPoints()); + + mPlayKmlButton.setEnabled(true); + mKmlBackwardButton.setEnabled(true); + mKmlForwardButton.setEnabled(true); + mKmlSpeedButton.setEnabled(true); + } + } + } + }); + + kmlWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + processCommandResult(mEmulatorConsole.sendLocation( + wayPoint.getLongitude(), wayPoint.getLatitude(), + wayPoint.getElevation())); + } + } + } + } + }); + + + + mKmlPlayControls = new Composite(kmlLocationComp, SWT.NONE); + GridLayout gl; + mKmlPlayControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + mKmlPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mPlayKmlButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT); + mPlayKmlButton.setImage(mPlayImage); + mPlayKmlButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mPlayingTrack == false) { + Object input = kmlWayPointViewer.getInput(); + if (input instanceof WayPoint[]) { + playKml((WayPoint[])input); + } + } else { + // if we're playing, then we pause + mPlayingTrack = false; + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + } + }); + + Label separator = new Label(mKmlPlayControls, SWT.SEPARATOR | SWT.VERTICAL); + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + + mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$ + mKmlBackwardButton.setSelection(false); + mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlForwardButton.setImage(mImageLoader.loadImage("forward.png", mParent.getDisplay())); // $NON-NLS-1$ + mKmlForwardButton.setSelection(true); + mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter); + + mKmlSpeedButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT); + + mSpeedIndex = 0; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mKmlSpeedButton.addSelectionListener(mSpeedButtonAdapter); + + mPlayKmlButton.setEnabled(false); + mKmlBackwardButton.setEnabled(false); + mKmlForwardButton.setEnabled(false); + mKmlSpeedButton.setEnabled(false); + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + } + + @Override + protected void postCreation() { + // pass + } + + private synchronized void setDataMode(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setGsmDataMode( + GsmMode.getEnum(GSM_MODES[selectionIndex][1]))); + } + } + + private synchronized void setVoiceMode(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setGsmVoiceMode( + GsmMode.getEnum(GSM_MODES[selectionIndex][1]))); + } + } + + private synchronized void setNetworkLatency(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setNetworkLatency(selectionIndex)); + } + } + + private synchronized void setNetworkSpeed(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setNetworkSpeed(selectionIndex)); + } + } + + + /** + * Callback on device selection change. + * @param device the new selected device + */ + public void handleNewDevice(Device device) { + if (mParent.isDisposed()) { + return; + } + // unlink to previous console. + synchronized (this) { + mEmulatorConsole = null; + } + + try { + // get the emulator console for this device + // First we need the device itself + if (device != null) { + GsmStatus gsm = null; + NetworkStatus netstatus = null; + + synchronized (this) { + mEmulatorConsole = EmulatorConsole.getConsole(device); + if (mEmulatorConsole != null) { + // get the gsm status + gsm = mEmulatorConsole.getGsmStatus(); + netstatus = mEmulatorConsole.getNetworkStatus(); + + if (gsm == null || netstatus == null) { + mEmulatorConsole = null; + } + } + } + + if (gsm != null && netstatus != null) { + Display d = mParent.getDisplay(); + if (d.isDisposed() == false) { + final GsmStatus f_gsm = gsm; + final NetworkStatus f_netstatus = netstatus; + + d.asyncExec(new Runnable() { + public void run() { + if (f_gsm.voice != GsmMode.UNKNOWN) { + mVoiceMode.select(getGsmComboIndex(f_gsm.voice)); + } else { + mVoiceMode.clearSelection(); + } + if (f_gsm.data != GsmMode.UNKNOWN) { + mDataMode.select(getGsmComboIndex(f_gsm.data)); + } else { + mDataMode.clearSelection(); + } + + if (f_netstatus.speed != -1) { + mNetworkSpeed.select(f_netstatus.speed); + } else { + mNetworkSpeed.clearSelection(); + } + + if (f_netstatus.latency != -1) { + mNetworkLatency.select(f_netstatus.latency); + } else { + mNetworkLatency.clearSelection(); + } + } + }); + } + } + } + } finally { + // enable/disable the ui + boolean enable = false; + synchronized (this) { + enable = mEmulatorConsole != null; + } + + enable(enable); + } + } + + /** + * Enable or disable the ui. Can be called from non ui threads. + * @param enabled + */ + private void enable(final boolean enabled) { + try { + Display d = mParent.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + if (mParent.isDisposed() == false) { + doEnable(enabled); + } + } + }); + } catch (SWTException e) { + // disposed. do nothing + } + } + + private boolean isValidPhoneNumber() { + String number = mPhoneNumber.getText().trim(); + + return number.matches(RE_PHONE_NUMBER); + } + + /** + * Enable or disable the ui. Cannot be called from non ui threads. + * @param enabled + */ + protected void doEnable(boolean enabled) { + mVoiceLabel.setEnabled(enabled); + mVoiceMode.setEnabled(enabled); + + mDataLabel.setEnabled(enabled); + mDataMode.setEnabled(enabled); + + mSpeedLabel.setEnabled(enabled); + mNetworkSpeed.setEnabled(enabled); + + mLatencyLabel.setEnabled(enabled); + mNetworkLatency.setEnabled(enabled); + + // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it + // if we don't need to. + if (mPhoneNumber.isEnabled() != enabled) { + mNumberLabel.setEnabled(enabled); + mPhoneNumber.setEnabled(enabled); + } + + boolean valid = isValidPhoneNumber(); + + mVoiceButton.setEnabled(enabled && valid); + mSmsButton.setEnabled(enabled && valid); + + boolean smsValid = enabled && valid && mSmsButton.getSelection(); + + // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it + // if we don't need to. + if (mSmsMessage.isEnabled() != smsValid) { + mMessageLabel.setEnabled(smsValid); + mSmsMessage.setEnabled(smsValid); + } + if (enabled == false) { + mSmsMessage.setText(""); //$NON-NLs-1$ + } + + mCallButton.setEnabled(enabled && valid); + mCancelButton.setEnabled(enabled && valid && mVoiceButton.getSelection()); + + if (enabled == false) { + mVoiceMode.clearSelection(); + mDataMode.clearSelection(); + mNetworkSpeed.clearSelection(); + mNetworkLatency.clearSelection(); + if (mPhoneNumber.getText().length() > 0) { + mPhoneNumber.setText(""); //$NON-NLS-1$ + } + } + + // location controls + mLocationFolders.setEnabled(enabled); + + mDecimalButton.setEnabled(enabled); + mSexagesimalButton.setEnabled(enabled); + mLongitudeControls.setEnabled(enabled); + mLatitudeControls.setEnabled(enabled); + + mGpxUploadButton.setEnabled(enabled); + mGpxWayPointTable.setEnabled(enabled); + mGpxTrackTable.setEnabled(enabled); + mKmlUploadButton.setEnabled(enabled); + mKmlWayPointTable.setEnabled(enabled); + } + + /** + * Returns the index of the combo item matching a specific GsmMode. + * @param mode + */ + private int getGsmComboIndex(GsmMode mode) { + for (int i = 0 ; i < GSM_MODES.length; i++) { + String[] modes = GSM_MODES[i]; + if (mode.getTag().equals(modes[1])) { + return i; + } + } + return -1; + } + + /** + * Processes the result of a command sent to the console. + * @param result the result of the command. + */ + private boolean processCommandResult(final String result) { + if (result != EmulatorConsole.RESULT_OK) { + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mParent.isDisposed() == false) { + MessageDialog.openError(mParent.getShell(), "Emulator Console", + result); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + + return false; + } + + return true; + } + + /** + * @param track + */ + private void playTrack(final Track track) { + // no need to synchronize this check, the worst that can happen, is we start the thread + // for nothing. + if (mEmulatorConsole != null) { + mPlayGpxButton.setImage(mPauseImage); + mPlayKmlButton.setImage(mPauseImage); + mPlayingTrack = true; + + mPlayingThread = new Thread() { + @Override + public void run() { + try { + TrackPoint[] trackPoints = track.getPoints(); + int count = trackPoints.length; + + // get the start index. + int start = 0; + if (mPlayDirection == -1) { + start = count - 1; + } + + for (int p = start; p >= 0 && p < count; p += mPlayDirection) { + if (mPlayingTrack == false) { + return; + } + + // get the current point and send its location to + // the emulator. + final TrackPoint trackPoint = trackPoints[p]; + + synchronized (EmulatorControlPanel.this) { + if (mEmulatorConsole == null || + processCommandResult(mEmulatorConsole.sendLocation( + trackPoint.getLongitude(), trackPoint.getLatitude(), + trackPoint.getElevation())) == false) { + return; + } + } + + // if this is not the final point, then get the next one and + // compute the delta time + int nextIndex = p + mPlayDirection; + if (nextIndex >=0 && nextIndex < count) { + TrackPoint nextPoint = trackPoints[nextIndex]; + + long delta = nextPoint.getTime() - trackPoint.getTime(); + if (delta < 0) { + delta = -delta; + } + + long startTime = System.currentTimeMillis(); + + try { + sleep(delta / mSpeed); + } catch (InterruptedException e) { + if (mPlayingTrack == false) { + return; + } + + // we got interrupted, lets make sure we can play + do { + long waited = System.currentTimeMillis() - startTime; + long needToWait = delta / mSpeed; + if (waited < needToWait) { + try { + sleep(needToWait - waited); + } catch (InterruptedException e1) { + // we'll just loop and wait again if needed. + // unless we're supposed to stop + if (mPlayingTrack == false) { + return; + } + } + } else { + break; + } + } while (true); + } + } + } + } finally { + mPlayingTrack = false; + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mPlayGpxButton.isDisposed() == false) { + mPlayGpxButton.setImage(mPlayImage); + mPlayKmlButton.setImage(mPlayImage); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + } + } + }; + + mPlayingThread.start(); + } + } + + private void playKml(final WayPoint[] trackPoints) { + // no need to synchronize this check, the worst that can happen, is we start the thread + // for nothing. + if (mEmulatorConsole != null) { + mPlayGpxButton.setImage(mPauseImage); + mPlayKmlButton.setImage(mPauseImage); + mPlayingTrack = true; + + mPlayingThread = new Thread() { + @Override + public void run() { + try { + int count = trackPoints.length; + + // get the start index. + int start = 0; + if (mPlayDirection == -1) { + start = count - 1; + } + + for (int p = start; p >= 0 && p < count; p += mPlayDirection) { + if (mPlayingTrack == false) { + return; + } + + // get the current point and send its location to + // the emulator. + WayPoint trackPoint = trackPoints[p]; + + synchronized (EmulatorControlPanel.this) { + if (mEmulatorConsole == null || + processCommandResult(mEmulatorConsole.sendLocation( + trackPoint.getLongitude(), trackPoint.getLatitude(), + trackPoint.getElevation())) == false) { + return; + } + } + + // if this is not the final point, then get the next one and + // compute the delta time + int nextIndex = p + mPlayDirection; + if (nextIndex >=0 && nextIndex < count) { + + long delta = 1000; // 1 second + if (delta < 0) { + delta = -delta; + } + + long startTime = System.currentTimeMillis(); + + try { + sleep(delta / mSpeed); + } catch (InterruptedException e) { + if (mPlayingTrack == false) { + return; + } + + // we got interrupted, lets make sure we can play + do { + long waited = System.currentTimeMillis() - startTime; + long needToWait = delta / mSpeed; + if (waited < needToWait) { + try { + sleep(needToWait - waited); + } catch (InterruptedException e1) { + // we'll just loop and wait again if needed. + // unless we're supposed to stop + if (mPlayingTrack == false) { + return; + } + } + } else { + break; + } + } while (true); + } + } + } + } finally { + mPlayingTrack = false; + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mPlayGpxButton.isDisposed() == false) { + mPlayGpxButton.setImage(mPlayImage); + mPlayKmlButton.setImage(mPlayImage); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + } + } + }; + + mPlayingThread.start(); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java new file mode 100644 index 0000000..977203b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java @@ -0,0 +1,1294 @@ +/* + * 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 com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.Log; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.CategoryAxis; +import org.jfree.chart.axis.CategoryLabelPositions; +import org.jfree.chart.labels.CategoryToolTipGenerator; +import org.jfree.chart.plot.CategoryPlot; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.category.CategoryItemRenderer; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.category.DefaultCategoryDataset; +import org.jfree.experimental.chart.swt.ChartComposite; +import org.jfree.experimental.swt.SWTUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + + +/** + * Base class for our information panels. + */ +public final class HeapPanel extends BaseHeapPanel { + private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$ + + /* args to setUpdateStatus() */ + private static final int NOT_SELECTED = 0; + private static final int NOT_ENABLED = 1; + private static final int ENABLED = 2; + + /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need + * Native+1 at least. We also need 2 more entries for free area and expansion area. */ + private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; + private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; + private static final PaletteData mMapPalette = createPalette(); + + private static final boolean DISPLAY_HEAP_BITMAP = false; + private static final boolean DISPLAY_HILBERT_BITMAP = false; + + private static final int PLACEHOLDER_HILBERT_SIZE = 200; + private static final int PLACEHOLDER_LINEAR_V_SIZE = 100; + private static final int PLACEHOLDER_LINEAR_H_SIZE = 300; + + private static final int[] ZOOMS = {100, 50, 25}; + + private static final NumberFormat sByteFormatter = NumberFormat.getInstance(); + private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance(); + private static final NumberFormat sCountFormatter = NumberFormat.getInstance(); + + static { + sByteFormatter.setMinimumFractionDigits(0); + sByteFormatter.setMaximumFractionDigits(1); + sLargeByteFormatter.setMinimumFractionDigits(3); + sLargeByteFormatter.setMaximumFractionDigits(3); + + sCountFormatter.setGroupingUsed(true); + } + + private Display mDisplay; + + private Composite mTop; // real top + private Label mUpdateStatus; + private Table mHeapSummary; + private Combo mDisplayMode; + + //private ScrolledComposite mScrolledComposite; + + private Composite mDisplayBase; // base of the displays. + private StackLayout mDisplayStack; + + private Composite mStatisticsBase; + private Table mStatisticsTable; + private JFreeChart mChart; + private ChartComposite mChartComposite; + private Button mGcButton; + private DefaultCategoryDataset mAllocCountDataSet; + + private Composite mLinearBase; + private Label mLinearHeapImage; + + private Composite mHilbertBase; + private Label mHilbertHeapImage; + private Group mLegend; + private Combo mZoom; + + /** Image used for the hilbert display. Since we recreate a new image every time, we + * keep this one around to dispose it. */ + private Image mHilbertImage; + private Image mLinearImage; + private Composite[] mLayout; + + /* + * Create color palette for map. Set up titles for legend. + */ + private static PaletteData createPalette() { + RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; + colors[0] + = new RGB(192, 192, 192); // non-heap pixels are gray + mMapLegend[0] + = "(heap expansion area)"; + + colors[1] + = new RGB(0, 0, 0); // free chunks are black + mMapLegend[1] + = "free"; + + colors[HeapSegmentElement.KIND_OBJECT + 2] + = new RGB(0, 0, 255); // objects are blue + mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] + = "data object"; + + colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = new RGB(0, 255, 0); // class objects are green + mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = "class object"; + + colors[HeapSegmentElement.KIND_ARRAY_1 + 2] + = new RGB(255, 0, 0); // byte/bool arrays are red + mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] + = "1-byte array (byte[], boolean[])"; + + colors[HeapSegmentElement.KIND_ARRAY_2 + 2] + = new RGB(255, 128, 0); // short/char arrays are orange + mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] + = "2-byte array (short[], char[])"; + + colors[HeapSegmentElement.KIND_ARRAY_4 + 2] + = new RGB(255, 255, 0); // obj/int/float arrays are yellow + mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] + = "4-byte array (object[], int[], float[])"; + + colors[HeapSegmentElement.KIND_ARRAY_8 + 2] + = new RGB(255, 128, 128); // long/double arrays are pink + mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] + = "8-byte array (long[], double[])"; + + colors[HeapSegmentElement.KIND_UNKNOWN + 2] + = new RGB(255, 0, 255); // unknown objects are cyan + mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] + = "unknown object"; + + colors[HeapSegmentElement.KIND_NATIVE + 2] + = new RGB(64, 64, 64); // native objects are dark gray + mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] + = "non-Java object"; + + return new PaletteData(colors); + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE || + (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) { + try { + mTop.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } catch (SWTException e) { + // display is disposed (app is quitting most likely), we do nothing. + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mTop.isDisposed()) + return; + + Client client = getCurrentClient(); + + Log.d("ddms", "HeapPanel: changed " + client); + + if (client != null) { + ClientData cd = client.getClientData(); + + if (client.isHeapUpdateEnabled()) { + mGcButton.setEnabled(true); + mDisplayMode.setEnabled(true); + setUpdateStatus(ENABLED); + } else { + setUpdateStatus(NOT_ENABLED); + mGcButton.setEnabled(false); + mDisplayMode.setEnabled(false); + } + + fillSummaryTable(cd); + + int mode = mDisplayMode.getSelectionIndex(); + if (mode == 0) { + fillDetailedTable(client, false /* forceRedraw */); + } else { + if (DISPLAY_HEAP_BITMAP) { + renderHeapData(cd, mode - 1, false /* forceRedraw */); + } + } + } else { + mGcButton.setEnabled(false); + mDisplayMode.setEnabled(false); + fillSummaryTable(null); + fillDetailedTable(null, true); + setUpdateStatus(NOT_SELECTED); + } + + // sizes of things change frequently, so redo layout + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + GridLayout gl; + + mTop = new Composite(parent, SWT.NONE); + mTop.setLayout(new GridLayout(1, false)); + mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mUpdateStatus = new Label(mTop, SWT.NONE); + setUpdateStatus(NOT_SELECTED); + + Composite summarySection = new Composite(mTop, SWT.NONE); + summarySection.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mHeapSummary = createSummaryTable(summarySection); + mGcButton = new Button(summarySection, SWT.PUSH); + mGcButton.setText("Cause GC"); + mGcButton.setEnabled(false); + mGcButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Client client = getCurrentClient(); + if (client != null) { + client.executeGarbageCollector(); + } + } + }); + + Composite comboSection = new Composite(mTop, SWT.NONE); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + comboSection.setLayout(gl); + + Label displayLabel = new Label(comboSection, SWT.NONE); + displayLabel.setText("Display: "); + + mDisplayMode = new Combo(comboSection, SWT.READ_ONLY); + mDisplayMode.setEnabled(false); + mDisplayMode.add("Stats"); + if (DISPLAY_HEAP_BITMAP) { + mDisplayMode.add("Linear"); + if (DISPLAY_HILBERT_BITMAP) { + mDisplayMode.add("Hilbert"); + } + } + + // the base of the displays. + mDisplayBase = new Composite(mTop, SWT.NONE); + mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDisplayStack = new StackLayout(); + mDisplayBase.setLayout(mDisplayStack); + + // create the statistics display + mStatisticsBase = new Composite(mDisplayBase, SWT.NONE); + //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + mStatisticsBase.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + mDisplayStack.topControl = mStatisticsBase; + + mStatisticsTable = createDetailedTable(mStatisticsBase); + mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createChart(); + + //create the linear composite + mLinearBase = new Composite(mDisplayBase, SWT.NONE); + //mLinearBase.setLayoutData(new GridData()); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + mLinearBase.setLayout(gl); + + { + mLinearHeapImage = new Label(mLinearBase, SWT.NONE); + mLinearHeapImage.setLayoutData(new GridData()); + mLinearHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay, + PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE))); + + // create a composite to contain the bottom part (legend) + Composite bottomSection = new Composite(mLinearBase, SWT.NONE); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + bottomSection.setLayout(gl); + + createLegend(bottomSection); + } + +/* + mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL); + mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + mScrolledComposite.setExpandHorizontal(true); + mScrolledComposite.setExpandVertical(true); + mScrolledComposite.setContent(mDisplayBase); +*/ + + + // create the hilbert display. + mHilbertBase = new Composite(mDisplayBase, SWT.NONE); + //mHilbertBase.setLayoutData(new GridData()); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + mHilbertBase.setLayout(gl); + + if (DISPLAY_HILBERT_BITMAP) { + mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE); + mHilbertHeapImage.setLayoutData(new GridData()); + mHilbertHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay, + PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE))); + + // create a composite to contain the right part (legend + zoom) + Composite rightSection = new Composite(mHilbertBase, SWT.NONE); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + rightSection.setLayout(gl); + + Composite zoomComposite = new Composite(rightSection, SWT.NONE); + gl = new GridLayout(2, false); + zoomComposite.setLayout(gl); + + Label l = new Label(zoomComposite, SWT.NONE); + l.setText("Zoom:"); + mZoom = new Combo(zoomComposite, SWT.READ_ONLY); + for (int z : ZOOMS) { + mZoom.add(String.format("%1$d%%", z)); // $NON-NLS-1$ + } + + mZoom.select(0); + mZoom.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + setLegendText(mZoom.getSelectionIndex()); + Client client = getCurrentClient(); + if (client != null) { + renderHeapData(client.getClientData(), 1, true); + mTop.pack(); + } + } + }); + + createLegend(rightSection); + } + mHilbertBase.pack(); + + mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase }; + mDisplayMode.select(0); + mDisplayMode.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = mDisplayMode.getSelectionIndex(); + Client client = getCurrentClient(); + + if (client != null) { + if (index == 0) { + fillDetailedTable(client, true /* forceRedraw */); + } else { + renderHeapData(client.getClientData(), index-1, true /* forceRedraw */); + } + } + + mDisplayStack.topControl = mLayout[index]; + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + } + }); + + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + + return mTop; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mHeapSummary.setFocus(); + } + + + private Table createSummaryTable(Composite base) { + Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); + tab.setHeaderVisible(true); + tab.setLinesVisible(true); + + TableColumn col; + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("ID"); + col.pack(); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Heap Size"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Allocated"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Free"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.00%"); // $NON-NLS-1$ + col.pack(); + col.setText("% Used"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000,000,000"); // $NON-NLS-1$ + col.pack(); + col.setText("# Objects"); + + return tab; + } + + private Table createDetailedTable(Composite base) { + IPreferenceStore store = DdmUiPreferences.getStore(); + + Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); + tab.setHeaderVisible(true); + tab.setLinesVisible(true); + + TableHelper.createTableColumn(tab, "Type", SWT.LEFT, + "4-byte array (object[], int[], float[])", //$NON-NLS-1$ + PREFS_STATS_COL_TYPE, store); + + TableHelper.createTableColumn(tab, "Count", SWT.RIGHT, + "00,000", //$NON-NLS-1$ + PREFS_STATS_COL_COUNT, store); + + TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_SIZE, store); + + TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_SMALLEST, store); + + TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_LARGEST, store); + + TableHelper.createTableColumn(tab, "Median", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_MEDIAN, store); + + TableHelper.createTableColumn(tab, "Average", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_AVERAGE, store); + + tab.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + + Client client = getCurrentClient(); + if (client != null) { + int index = mStatisticsTable.getSelectionIndex(); + TableItem item = mStatisticsTable.getItem(index); + + if (item != null) { + Map<Integer, ArrayList<HeapSegmentElement>> heapMap = + client.getClientData().getVmHeapData().getProcessedHeapMap(); + + ArrayList<HeapSegmentElement> list = heapMap.get(item.getData()); + if (list != null) { + showChart(list); + } + } + } + + } + }); + + return tab; + } + + /** + * Creates the chart below the statistics table + */ + private void createChart() { + mAllocCountDataSet = new DefaultCategoryDataset(); + mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet, + PlotOrientation.VERTICAL, false, true, false); + + // get the font to make a proper title. We need to convert the swt font, + // into an awt font. + Font f = mStatisticsBase.getFont(); + FontData[] fData = f.getFontData(); + + // event though on Mac OS there could be more than one fontData, we'll only use + // the first one. + FontData firstFontData = fData[0]; + + java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(), + firstFontData, true /* ensureSameSize */); + + mChart.setTitle(new TextTitle("Allocation count per size", awtFont)); + + Plot plot = mChart.getPlot(); + if (plot instanceof CategoryPlot) { + // get the plot + CategoryPlot categoryPlot = (CategoryPlot)plot; + + // set the domain axis to draw labels that are displayed even with many values. + CategoryAxis domainAxis = categoryPlot.getDomainAxis(); + domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90); + + CategoryItemRenderer renderer = categoryPlot.getRenderer(); + renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() { + public String generateToolTip(CategoryDataset dataset, int row, int column) { + // get the key for the size of the allocation + ByteLong columnKey = (ByteLong)dataset.getColumnKey(column); + String rowKey = (String)dataset.getRowKey(row); + Number value = dataset.getValue(rowKey, columnKey); + + return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey, + columnKey.getValue()); + } + }); + } + mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, // max draw width. We don't want it to zoom, so we put a big number + 3000, // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + false, // zoom + true); // tooltips + + mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + } + + private static String prettyByteCount(long bytes) { + double fracBytes = bytes; + String units = " B"; + if (fracBytes < 1024) { + return sByteFormatter.format(fracBytes) + units; + } else { + fracBytes /= 1024; + units = " KB"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = " MB"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = " GB"; + } + + return sLargeByteFormatter.format(fracBytes) + units; + } + + private static String approximateByteCount(long bytes) { + double fracBytes = bytes; + String units = ""; + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "K"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "M"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "G"; + } + + return sByteFormatter.format(fracBytes) + units; + } + + private static String addCommasToNumber(long num) { + return sCountFormatter.format(num); + } + + private static String fractionalPercent(long num, long denom) { + double val = (double)num / (double)denom; + val *= 100; + + NumberFormat nf = NumberFormat.getInstance(); + nf.setMinimumFractionDigits(2); + nf.setMaximumFractionDigits(2); + return nf.format(val) + "%"; + } + + private void fillSummaryTable(ClientData cd) { + if (mHeapSummary.isDisposed()) { + return; + } + + mHeapSummary.setRedraw(false); + mHeapSummary.removeAll(); + + if (cd != null) { + synchronized (cd) { + Iterator<Integer> iter = cd.getVmHeapIds(); + + while (iter.hasNext()) { + Integer id = iter.next(); + Map<String, Long> heapInfo = cd.getVmHeapInfo(id); + if (heapInfo == null) { + continue; + } + long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES); + long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED); + long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED); + + TableItem item = new TableItem(mHeapSummary, SWT.NONE); + item.setText(0, id.toString()); + + item.setText(1, prettyByteCount(sizeInBytes)); + item.setText(2, prettyByteCount(bytesAllocated)); + item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated)); + item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes)); + item.setText(5, addCommasToNumber(objectsAllocated)); + } + } + } + + mHeapSummary.pack(); + mHeapSummary.setRedraw(true); + } + + private void fillDetailedTable(Client client, boolean forceRedraw) { + // first check if the client is invalid or heap updates are not enabled. + if (client == null || client.isHeapUpdateEnabled() == false) { + mStatisticsTable.removeAll(); + showChart(null); + return; + } + + ClientData cd = client.getClientData(); + + Map<Integer, ArrayList<HeapSegmentElement>> heapMap; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { + // no change, we return. + return; + } + + heapMap = cd.getVmHeapData().getProcessedHeapMap(); + } + + // we have new data, lets display it. + + // First, get the current selection, and its key. + int index = mStatisticsTable.getSelectionIndex(); + Integer selectedKey = null; + if (index != -1) { + selectedKey = (Integer)mStatisticsTable.getItem(index).getData(); + } + + // disable redraws and remove all from the table. + mStatisticsTable.setRedraw(false); + mStatisticsTable.removeAll(); + + if (heapMap != null) { + int selectedIndex = -1; + ArrayList<HeapSegmentElement> selectedList = null; + + // get the keys + Set<Integer> keys = heapMap.keySet(); + int iter = 0; // use a manual iter int because Set<?> doesn't have an index + // based accessor. + for (Integer key : keys) { + ArrayList<HeapSegmentElement> list = heapMap.get(key); + + // check if this is the key that is supposed to be selected + if (key.equals(selectedKey)) { + selectedIndex = iter; + selectedList = list; + } + iter++; + + TableItem item = new TableItem(mStatisticsTable, SWT.NONE); + item.setData(key); + + // get the type + item.setText(0, mMapLegend[key]); + + // set the count, smallest, largest + int count = list.size(); + item.setText(1, addCommasToNumber(count)); + + if (count > 0) { + item.setText(3, prettyByteCount(list.get(0).getLength())); + item.setText(4, prettyByteCount(list.get(count-1).getLength())); + + int median = count / 2; + HeapSegmentElement element = list.get(median); + long size = element.getLength(); + item.setText(5, prettyByteCount(size)); + + long totalSize = 0; + for (int i = 0 ; i < count; i++) { + element = list.get(i); + + size = element.getLength(); + totalSize += size; + } + + // set the average and total + item.setText(2, prettyByteCount(totalSize)); + item.setText(6, prettyByteCount(totalSize / count)); + } + } + + mStatisticsTable.setRedraw(true); + + if (selectedIndex != -1) { + mStatisticsTable.setSelection(selectedIndex); + showChart(selectedList); + } else { + showChart(null); + } + } else { + mStatisticsTable.setRedraw(true); + } + } + + private static class ByteLong implements Comparable<ByteLong> { + private long mValue; + + private ByteLong(long value) { + mValue = value; + } + + public long getValue() { + return mValue; + } + + @Override + public String toString() { + return approximateByteCount(mValue); + } + + public int compareTo(ByteLong other) { + if (mValue != other.mValue) { + return mValue < other.mValue ? -1 : 1; + } + return 0; + } + + } + + /** + * Fills the chart with the content of the list of {@link HeapSegmentElement}. + */ + private void showChart(ArrayList<HeapSegmentElement> list) { + mAllocCountDataSet.clear(); + + if (list != null) { + String rowKey = "Alloc Count"; + + long currentSize = -1; + int currentCount = 0; + for (HeapSegmentElement element : list) { + if (element.getLength() != currentSize) { + if (currentSize != -1) { + ByteLong columnKey = new ByteLong(currentSize); + mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); + } + + currentSize = element.getLength(); + currentCount = 1; + } else { + currentCount++; + } + } + + // add the last item + if (currentSize != -1) { + ByteLong columnKey = new ByteLong(currentSize); + mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); + } + } + } + + /* + * Add a color legend to the specified table. + */ + private void createLegend(Composite parent) { + mLegend = new Group(parent, SWT.NONE); + mLegend.setText(getLegendText(0)); + + mLegend.setLayout(new GridLayout(2, false)); + + RGB[] colors = mMapPalette.colors; + + for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) { + Image tmpImage = createColorRect(parent.getDisplay(), colors[i]); + + Label l = new Label(mLegend, SWT.NONE); + l.setImage(tmpImage); + + l = new Label(mLegend, SWT.NONE); + l.setText(mMapLegend[i]); + } + } + + private String getLegendText(int level) { + int bytes = 8 * (100 / ZOOMS[level]); + + return String.format("Key (1 pixel = %1$d bytes)", bytes); + } + + private void setLegendText(int level) { + mLegend.setText(getLegendText(level)); + + } + + /* + * Create a nice rectangle in the specified color. + */ + private Image createColorRect(Display display, RGB color) { + int width = 32; + int height = 16; + + Image img = new Image(display, width, height); + GC gc = new GC(img); + gc.setBackground(new Color(display, color)); + gc.fillRectangle(0, 0, width, height); + gc.dispose(); + return img; + } + + + /* + * Are updates enabled? + */ + private void setUpdateStatus(int status) { + switch (status) { + case NOT_SELECTED: + mUpdateStatus.setText("Select a client to see heap updates"); + break; + case NOT_ENABLED: + mUpdateStatus.setText("Heap updates are " + + "NOT ENABLED for this client"); + break; + case ENABLED: + mUpdateStatus.setText("Heap updates will happen after " + + "every GC for this client"); + break; + default: + throw new RuntimeException(); + } + + mUpdateStatus.pack(); + } + + + /** + * Return the closest power of two greater than or equal to value. + * + * @param value the return value will be >= value + * @return a power of two >= value. If value > 2^31, 2^31 is returned. + */ +//xxx use Integer.highestOneBit() or numberOfLeadingZeros(). + private int nextPow2(int value) { + for (int i = 31; i >= 0; --i) { + if ((value & (1<<i)) != 0) { + if (i < 31) { + return 1<<(i + 1); + } else { + return 1<<31; + } + } + } + return 0; + } + + private int zOrderData(ImageData id, byte pixData[]) { + int maxX = 0; + for (int i = 0; i < pixData.length; i++) { + /* Tread the pixData index as a z-order curve index and + * decompose into Cartesian coordinates. + */ + int x = (i & 1) | + ((i >>> 2) & 1) << 1 | + ((i >>> 4) & 1) << 2 | + ((i >>> 6) & 1) << 3 | + ((i >>> 8) & 1) << 4 | + ((i >>> 10) & 1) << 5 | + ((i >>> 12) & 1) << 6 | + ((i >>> 14) & 1) << 7 | + ((i >>> 16) & 1) << 8 | + ((i >>> 18) & 1) << 9 | + ((i >>> 20) & 1) << 10 | + ((i >>> 22) & 1) << 11 | + ((i >>> 24) & 1) << 12 | + ((i >>> 26) & 1) << 13 | + ((i >>> 28) & 1) << 14 | + ((i >>> 30) & 1) << 15; + int y = ((i >>> 1) & 1) << 0 | + ((i >>> 3) & 1) << 1 | + ((i >>> 5) & 1) << 2 | + ((i >>> 7) & 1) << 3 | + ((i >>> 9) & 1) << 4 | + ((i >>> 11) & 1) << 5 | + ((i >>> 13) & 1) << 6 | + ((i >>> 15) & 1) << 7 | + ((i >>> 17) & 1) << 8 | + ((i >>> 19) & 1) << 9 | + ((i >>> 21) & 1) << 10 | + ((i >>> 23) & 1) << 11 | + ((i >>> 25) & 1) << 12 | + ((i >>> 27) & 1) << 13 | + ((i >>> 29) & 1) << 14 | + ((i >>> 31) & 1) << 15; + try { + id.setPixel(x, y, pixData[i]); + if (x > maxX) { + maxX = x; + } + } catch (IllegalArgumentException ex) { + System.out.println("bad pixels: i " + i + + ", w " + id.width + + ", h " + id.height + + ", x " + x + + ", y " + y); + throw ex; + } + } + return maxX; + } + + private final static int HILBERT_DIR_N = 0; + private final static int HILBERT_DIR_S = 1; + private final static int HILBERT_DIR_E = 2; + private final static int HILBERT_DIR_W = 3; + + private void hilbertWalk(ImageData id, InputStream pixData, + int order, int x, int y, int dir) + throws IOException { + if (x >= id.width || y >= id.height) { + return; + } else if (order == 0) { + try { + int p = pixData.read(); + if (p >= 0) { + // flip along x=y axis; assume width == height + id.setPixel(y, x, p); + + /* Skanky; use an otherwise-unused ImageData field + * to keep track of the max x,y used. Note that x and y are inverted. + */ + if (y > id.x) { + id.x = y; + } + if (x > id.y) { + id.y = x; + } + } +//xxx just give up; don't bother walking the rest of the image + } catch (IllegalArgumentException ex) { + System.out.println("bad pixels: order " + order + + ", dir " + dir + + ", w " + id.width + + ", h " + id.height + + ", x " + x + + ", y " + y); + throw ex; + } + } else { + order--; + int delta = 1 << order; + int nextX = x + delta; + int nextY = y + delta; + + switch (dir) { + case HILBERT_DIR_E: + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_S); + break; + case HILBERT_DIR_N: + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_W); + break; + case HILBERT_DIR_S: + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_E); + break; + case HILBERT_DIR_W: + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_N); + break; + default: + throw new RuntimeException("Unexpected Hilbert direction " + + dir); + } + } + } + + private Point hilbertOrderData(ImageData id, byte pixData[]) { + + int order = 0; + for (int n = 1; n < id.width; n *= 2) { + order++; + } + /* Skanky; use an otherwise-unused ImageData field + * to keep track of maxX. + */ + Point p = new Point(0,0); + int oldIdX = id.x; + int oldIdY = id.y; + id.x = id.y = 0; + try { + hilbertWalk(id, new ByteArrayInputStream(pixData), + order, 0, 0, HILBERT_DIR_E); + p.x = id.x; + p.y = id.y; + } catch (IOException ex) { + System.err.println("Exception during hilbertWalk()"); + p.x = id.height; + p.y = id.width; + } + id.x = oldIdX; + id.y = oldIdY; + return p; + } + + private ImageData createHilbertHeapImage(byte pixData[]) { + int w, h; + + // Pick an image size that the largest of heaps will fit into. + w = (int)Math.sqrt((double)((16 * 1024 * 1024)/8)); + + // Space-filling curves require a power-of-2 width. + w = nextPow2(w); + h = w; + + // Create the heap image. + ImageData id = new ImageData(w, h, 8, mMapPalette); + + // Copy the data into the image + //int maxX = zOrderData(id, pixData); + Point maxP = hilbertOrderData(id, pixData); + + // update the max size to make it a round number once the zoom is applied + int factor = 100 / ZOOMS[mZoom.getSelectionIndex()]; + if (factor != 1) { + int tmp = maxP.x % factor; + if (tmp != 0) { + maxP.x += factor - tmp; + } + + tmp = maxP.y % factor; + if (tmp != 0) { + maxP.y += factor - tmp; + } + } + + if (maxP.y < id.height) { + // Crop the image down to the interesting part. + id = new ImageData(id.width, maxP.y, id.depth, id.palette, + id.scanlinePad, id.data); + } + + if (maxP.x < id.width) { + // crop the image again. A bit trickier this time. + ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette); + + int[] buffer = new int[maxP.x]; + for (int l = 0 ; l < id.height; l++) { + id.getPixels(0, l, maxP.x, buffer, 0); + croppedId.setPixels(0, l, maxP.x, buffer, 0); + } + + id = croppedId; + } + + // apply the zoom + if (factor != 1) { + id = id.scaledTo(id.width / factor, id.height / factor); + } + + return id; + } + + /** + * Convert the raw heap data to an image. We know we're running in + * the UI thread, so we can issue graphics commands directly. + * + * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html + * + * @param cd The client data + * @param mode The display mode. 0 = linear, 1 = hilbert. + * @param forceRedraw + */ + private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) { + Image image; + + byte[] pixData; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { + // no change, we return. + return; + } + + pixData = getSerializedData(); + } + + if (pixData != null) { + ImageData id; + if (mode == 1) { + id = createHilbertHeapImage(pixData); + } else { + id = createLinearHeapImage(pixData, 200, mMapPalette); + } + + image = new Image(mDisplay, id); + } else { + // Render a placeholder image. + int width, height; + if (mode == 1) { + width = height = PLACEHOLDER_HILBERT_SIZE; + } else { + width = PLACEHOLDER_LINEAR_H_SIZE; + height = PLACEHOLDER_LINEAR_V_SIZE; + } + image = new Image(mDisplay, width, height); + GC gc = new GC(image); + gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED)); + gc.drawLine(0, 0, width-1, height-1); + gc.dispose(); + gc = null; + } + + // set the new image + + if (mode == 1) { + if (mHilbertImage != null) { + mHilbertImage.dispose(); + } + + mHilbertImage = image; + mHilbertHeapImage.setImage(mHilbertImage); + mHilbertHeapImage.pack(true); + mHilbertBase.layout(); + mHilbertBase.pack(true); + } else { + if (mLinearImage != null) { + mLinearImage.dispose(); + } + + mLinearImage = image; + mLinearHeapImage.setImage(mLinearImage); + mLinearHeapImage.pack(true); + mLinearBase.layout(); + mLinearBase.pack(true); + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mHeapSummary); + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java new file mode 100644 index 0000000..bcbf612 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java @@ -0,0 +1,45 @@ +/* + * 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 org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +/** + * Interface defining an image loader. jar app/lib and plugin have different packaging method + * so each implementation will be different. + * The implementation should implement at least one of the methods, and preferably both if possible. + * + */ +public interface IImageLoader { + + /** + * Load an image from the resource from a filename + * @param filename + * @param display + */ + public Image loadImage(String filename, Display display); + + /** + * Load an ImageDescriptor from the resource from a filename + * @param filename + * @param display + */ + public ImageDescriptor loadDescriptor(String filename, Display display); + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java new file mode 100644 index 0000000..bf425d9 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java @@ -0,0 +1,38 @@ +/* + * 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 org.eclipse.swt.dnd.Clipboard; + +/** + * An object listening to focus change in Table objects.<br> + * For application not relying on a RCP to provide menu changes based on focus, + * this class allows to get monitor the focus change of several Table widget + * and update the menu action accordingly. + */ +public interface ITableFocusListener { + + public interface IFocusedTableActivator { + public void copy(Clipboard clipboard); + + public void selectAll(); + } + + public void focusGained(IFocusedTableActivator activator); + + public void focusLost(IFocusedTableActivator activator); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java new file mode 100644 index 0000000..d65978b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java @@ -0,0 +1,86 @@ +/* + * 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 com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class ImageHelper { + + /** + * Loads an image from a resource. This method used a class to locate the + * resources, and then load the filename from /images inside the resources.<br> + * Extra parameters allows for creation of a replacement image of the + * loading failed. + * + * @param loader the image loader used. + * @param display the Display object + * @param fileName the file name + * @param width optional width to create replacement Image. If -1, null be + * be returned if the loading fails. + * @param height optional height to create replacement Image. If -1, null be + * be returned if the loading fails. + * @param phColor optional color to create replacement Image. If null, Blue + * color will be used. + * @return a new Image or null if the loading failed and the optional + * replacement size was -1 + */ + public static Image loadImage(IImageLoader loader, Display display, + String fileName, int width, int height, Color phColor) { + + Image img = null; + if (loader != null) { + img = loader.loadImage(fileName, display); + } + + if (img == null) { + Log.w("ddms", "Couldn't load " + fileName); + // if we had the extra parameter to create replacement image then we + // create and return it. + if (width != -1 && height != -1) { + return createPlaceHolderArt(display, width, height, + phColor != null ? phColor : display + .getSystemColor(SWT.COLOR_BLUE)); + } + + // otherwise, just return null + return null; + } + + return img; + } + + /** + * Create place-holder art with the specified color. + */ + public static Image createPlaceHolderArt(Display display, int width, + int height, Color color) { + Image img = new Image(display, width, height); + GC gc = new GC(img); + gc.setForeground(color); + gc.drawLine(0, 0, width, height); + gc.drawLine(0, height - 1, width, -1); + gc.dispose(); + return img; + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java new file mode 100644 index 0000000..76f2285 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java @@ -0,0 +1,62 @@ +/* + * 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 org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.io.InputStream; + +/** + * Image loader for an normal standalone app. + */ +public class ImageLoader implements IImageLoader { + + /** class used as reference to get the reources */ + private Class<?> mClass; + + /** + * Creates a loader for a specific class. The class allows java to figure + * out which .jar file to search for the image. + * + * @param theClass + */ + public ImageLoader(Class<?> theClass) { + mClass = theClass; + } + + public ImageDescriptor loadDescriptor(String filename, Display display) { + // we don't support ImageDescriptor + return null; + } + + public Image loadImage(String filename, Display display) { + + String tmp = "/images/" + filename; + InputStream imageStream = mClass.getResourceAsStream(tmp); + + if (imageStream != null) { + Image img = new Image(display, imageStream); + if (img == null) + throw new NullPointerException("couldn't load " + tmp); + return img; + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java new file mode 100644 index 0000000..72cbb4a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java @@ -0,0 +1,175 @@ +/* + * 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 com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +/** + * Display client info in a two-column format. + */ +public class InfoPanel extends TablePanel { + private Table mTable; + private TableColumn mCol2; + + private static final String mLabels[] = { + "DDM-aware?", + "App description:", + "VM version:", + "Process ID:", + }; + private static final int ENT_DDM_AWARE = 0; + private static final int ENT_APP_DESCR = 1; + private static final int ENT_VM_VERSION = 2; + private static final int ENT_PROCESS_ID = 3; + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mTable.setHeaderVisible(false); + mTable.setLinesVisible(false); + + TableColumn col1 = new TableColumn(mTable, SWT.RIGHT); + col1.setText("name"); + mCol2 = new TableColumn(mTable, SWT.LEFT); + mCol2.setText("PlaceHolderContentForWidth"); + + TableItem item; + for (int i = 0; i < mLabels.length; i++) { + item = new TableItem(mTable, SWT.NONE); + item.setText(0, mLabels[i]); + item.setText(1, "-"); + } + + col1.pack(); + mCol2.pack(); + + return mTable; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTable.setFocus(); + } + + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_PORT}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_INFO) == Client.CHANGE_INFO) { + if (mTable.isDisposed()) + return; + + mTable.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } + } + } + + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()} + */ + @Override + public void clientSelected() { + if (mTable.isDisposed()) + return; + + Client client = getCurrentClient(); + + if (client == null) { + for (int i = 0; i < mLabels.length; i++) { + TableItem item = mTable.getItem(i); + item.setText(1, "-"); + } + } else { + TableItem item; + String clientDescription, vmIdentifier, isDdmAware, + pid; + + ClientData cd = client.getClientData(); + synchronized (cd) { + clientDescription = (cd.getClientDescription() != null) ? + cd.getClientDescription() : "?"; + vmIdentifier = (cd.getVmIdentifier() != null) ? + cd.getVmIdentifier() : "?"; + isDdmAware = cd.isDdmAware() ? + "yes" : "no"; + pid = (cd.getPid() != 0) ? + String.valueOf(cd.getPid()) : "?"; + } + + item = mTable.getItem(ENT_APP_DESCR); + item.setText(1, clientDescription); + item = mTable.getItem(ENT_VM_VERSION); + item.setText(1, vmIdentifier); + item = mTable.getItem(ENT_DDM_AWARE); + item.setText(1, isDdmAware); + item = mTable.getItem(ENT_PROCESS_ID); + item.setText(1, pid); + } + + mCol2.pack(); + + //Log.i("ddms", "InfoPanel: changed " + client); + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mTable); + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java new file mode 100644 index 0000000..46461bf --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java @@ -0,0 +1,1633 @@ +/* + * 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 com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.Log; +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeLibraryMapInfo; +import com.android.ddmlib.NativeStackCallInfo; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; +import com.android.ddmuilib.annotation.WorkerThread; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +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.Sash; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; + +/** + * Panel with native heap information. + */ +public final class NativeHeapPanel extends BaseHeapPanel { + + /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need + * Native+1 at least. We also need 2 more entries for free area and expansion area. */ + private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; + private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; + private static final PaletteData mMapPalette = createPalette(); + + private static final int ALLOC_DISPLAY_ALL = 0; + private static final int ALLOC_DISPLAY_PRE_ZYGOTE = 1; + private static final int ALLOC_DISPLAY_POST_ZYGOTE = 2; + + private Display mDisplay; + + private Composite mBase; + + private Label mUpdateStatus; + + /** combo giving choice of what to display: all, pre-zygote, post-zygote */ + private Combo mAllocDisplayCombo; + + private Button mFullUpdateButton; + + // see CreateControl() + //private Button mDiffUpdateButton; + + private Combo mDisplayModeCombo; + + /** stack composite for mode (1-2) & 3 */ + private Composite mTopStackComposite; + + private StackLayout mTopStackLayout; + + /** stack composite for mode 1 & 2 */ + private Composite mAllocationStackComposite; + + private StackLayout mAllocationStackLayout; + + /** top level container for mode 1 & 2 */ + private Composite mTableModeControl; + + /** top level object for the allocation mode */ + private Control mAllocationModeTop; + + /** top level for the library mode */ + private Control mLibraryModeTopControl; + + /** composite for page UI and total memory display */ + private Composite mPageUIComposite; + + private Label mTotalMemoryLabel; + + private Label mPageLabel; + + private Button mPageNextButton; + + private Button mPagePreviousButton; + + private Table mAllocationTable; + + private Table mLibraryTable; + + private Table mLibraryAllocationTable; + + private Table mDetailTable; + + private Label mImage; + + private int mAllocDisplayMode = ALLOC_DISPLAY_ALL; + + /** + * pointer to current stackcall thread computation in order to quit it if + * required (new update requested) + */ + private StackCallThread mStackCallThread; + + /** Current Library Allocation table fill thread. killed if selection changes */ + private FillTableThread mFillTableThread; + + /** + * current client data. Used to access the malloc info when switching pages + * or selecting allocation to show stack call + */ + private ClientData mClientData; + + /** + * client data from a previous display. used when asking for an "update & diff" + */ + private ClientData mBackUpClientData; + + /** list of NativeAllocationInfo objects filled with the list from ClientData */ + private final ArrayList<NativeAllocationInfo> mAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** list of the {@link NativeAllocationInfo} being displayed based on the selection + * of {@link #mAllocDisplayCombo}. + */ + private final ArrayList<NativeAllocationInfo> mDisplayedAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */ + private final ArrayList<NativeAllocationInfo> mBackUpAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** back up of the total memory, used when doing an "update & diff" */ + private int mBackUpTotalMemory; + + private int mCurrentPage = 0; + + private int mPageCount = 0; + + /** + * list of allocation per Library. This is created from the list of + * NativeAllocationInfo objects that is stored in the ClientData object. Since we + * don't keep this list around, it is recomputed everytime the client + * changes. + */ + private final ArrayList<LibraryAllocations> mLibraryAllocations = + new ArrayList<LibraryAllocations>(); + + /* args to setUpdateStatus() */ + private static final int NOT_SELECTED = 0; + + private static final int NOT_ENABLED = 1; + + private static final int ENABLED = 2; + + private static final int DISPLAY_PER_PAGE = 20; + + private static final String PREFS_ALLOCATION_SASH = "NHallocSash"; //$NON-NLS-1$ + private static final String PREFS_LIBRARY_SASH = "NHlibrarySash"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_ADDRESS = "NHdetailAddress"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_LIBRARY = "NHdetailLibrary"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_METHOD = "NHdetailMethod"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_FILE = "NHdetailFile"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_LINE = "NHdetailLine"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_TOTAL = "NHallocTotal"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_COUNT = "NHallocCount"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_SIZE = "NHallocSize"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_LIBRARY = "NHallocLib"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_METHOD = "NHallocMethod"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_FILE = "NHallocFile"; //$NON-NLS-1$ + private static final String PREFS_LIB_LIBRARY = "NHlibLibrary"; //$NON-NLS-1$ + private static final String PREFS_LIB_SIZE = "NHlibSize"; //$NON-NLS-1$ + private static final String PREFS_LIB_COUNT = "NHlibCount"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_TOTAL = "NHlibAllocTotal"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_COUNT = "NHlibAllocCount"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_SIZE = "NHlibAllocSize"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_METHOD = "NHlibAllocMethod"; //$NON-NLS-1$ + + /** static formatter object to format all numbers as #,### */ + private static DecimalFormat sFormatter; + static { + sFormatter = (DecimalFormat)NumberFormat.getInstance(); + if (sFormatter != null) + sFormatter = new DecimalFormat("#,###"); + else + sFormatter.applyPattern("#,###"); + } + + + /** + * caching mechanism to avoid recomputing the backtrace for a particular + * address several times. + */ + private HashMap<Long, NativeStackCallInfo> mSourceCache = + new HashMap<Long,NativeStackCallInfo>(); + private long mTotalSize; + private Button mSaveButton; + private Button mSymbolsButton; + + /** + * thread class to convert the address call into method, file and line + * number in the background. + */ + private class StackCallThread extends BackgroundThread { + private ClientData mClientData; + + public StackCallThread(ClientData cd) { + mClientData = cd; + } + + public ClientData getClientData() { + return mClientData; + } + + @Override + public void run() { + // loop through all the NativeAllocationInfo and init them + Iterator<NativeAllocationInfo> iter = mAllocations.iterator(); + int total = mAllocations.size(); + int count = 0; + while (iter.hasNext()) { + + if (isQuitting()) + return; + + NativeAllocationInfo info = iter.next(); + if (info.isStackCallResolved() == false) { + final Long[] list = info.getStackCallAddresses(); + final int size = list.length; + + ArrayList<NativeStackCallInfo> resolvedStackCall = + new ArrayList<NativeStackCallInfo>(); + + for (int i = 0; i < size; i++) { + long addr = list[i]; + + // first check if the addr has already been converted. + NativeStackCallInfo source = mSourceCache.get(addr); + + // if not we convert it + if (source == null) { + source = sourceForAddr(addr); + mSourceCache.put(addr, source); + } + + resolvedStackCall.add(source); + } + + info.setResolvedStackCall(resolvedStackCall); + } + // after every DISPLAY_PER_PAGE we ask for a ui refresh, unless + // we reach total, since we also do it after the loop + // (only an issue in case we have a perfect number of page) + count++; + if ((count % DISPLAY_PER_PAGE) == 0 && count != total) { + if (updateNHAllocationStackCalls(mClientData, count) == false) { + // looks like the app is quitting, so we just + // stopped the thread + return; + } + } + } + + updateNHAllocationStackCalls(mClientData, count); + } + + private NativeStackCallInfo sourceForAddr(long addr) { + NativeLibraryMapInfo library = getLibraryFor(addr); + + if (library != null) { + + Addr2Line process = Addr2Line.getProcess(library.getLibraryName()); + if (process != null) { + // remove the base of the library address + long value = addr - library.getStartAddress(); + NativeStackCallInfo info = process.getAddress(value); + if (info != null) { + return info; + } + } + } + + return new NativeStackCallInfo(library != null ? library.getLibraryName() : null, + Long.toHexString(addr), ""); + } + + private NativeLibraryMapInfo getLibraryFor(long addr) { + Iterator<NativeLibraryMapInfo> it = mClientData.getNativeLibraryMapInfo(); + + while (it.hasNext()) { + NativeLibraryMapInfo info = it.next(); + + if (info.isWithinLibrary(addr)) { + return info; + } + } + + Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr)); + return null; + } + + /** + * update the Native Heap panel with the amount of allocation for which the + * stack call has been computed. This is called from a non UI thread, but + * will be executed in the UI thread. + * + * @param count the amount of allocation + * @return false if the display was disposed and the update couldn't happen + */ + private boolean updateNHAllocationStackCalls(final ClientData clientData, final int count) { + if (mDisplay.isDisposed() == false) { + mDisplay.asyncExec(new Runnable() { + public void run() { + updateAllocationStackCalls(clientData, count); + } + }); + return true; + } + return false; + } + } + + private class FillTableThread extends BackgroundThread { + private LibraryAllocations mLibAlloc; + + private int mMax; + + public FillTableThread(LibraryAllocations liballoc, int m) { + mLibAlloc = liballoc; + mMax = m; + } + + @Override + public void run() { + for (int i = mMax; i > 0 && isQuitting() == false; i -= 10) { + updateNHLibraryAllocationTable(mLibAlloc, mMax - i, mMax - i + 10); + } + } + + /** + * updates the library allocation table in the Native Heap panel. This is + * called from a non UI thread, but will be executed in the UI thread. + * + * @param liballoc the current library allocation object being displayed + * @param start start index of items that need to be displayed + * @param end end index of the items that need to be displayed + */ + private void updateNHLibraryAllocationTable(final LibraryAllocations libAlloc, + final int start, final int end) { + if (mDisplay.isDisposed() == false) { + mDisplay.asyncExec(new Runnable() { + public void run() { + updateLibraryAllocationTable(libAlloc, start, end); + } + }); + } + + } + } + + /** class to aggregate allocations per library */ + public static class LibraryAllocations { + private String mLibrary; + + private final ArrayList<NativeAllocationInfo> mLibAllocations = + new ArrayList<NativeAllocationInfo>(); + + private int mSize; + + private int mCount; + + /** construct the aggregate object for a library */ + public LibraryAllocations(final String lib) { + mLibrary = lib; + } + + /** get the library name */ + public String getLibrary() { + return mLibrary; + } + + /** add a NativeAllocationInfo object to this aggregate object */ + public void addAllocation(NativeAllocationInfo info) { + mLibAllocations.add(info); + } + + /** get an iterator on the NativeAllocationInfo objects */ + public Iterator<NativeAllocationInfo> getAllocations() { + return mLibAllocations.iterator(); + } + + /** get a NativeAllocationInfo object by index */ + public NativeAllocationInfo getAllocation(int index) { + return mLibAllocations.get(index); + } + + /** returns the NativeAllocationInfo object count */ + public int getAllocationSize() { + return mLibAllocations.size(); + } + + /** returns the total allocation size */ + public int getSize() { + return mSize; + } + + /** returns the number of allocations */ + public int getCount() { + return mCount; + } + + /** + * compute the allocation count and size for allocation objects added + * through <code>addAllocation()</code>, and sort the objects by + * total allocation size. + */ + public void computeAllocationSizeAndCount() { + mSize = 0; + mCount = 0; + for (NativeAllocationInfo info : mLibAllocations) { + mCount += info.getAllocationCount(); + mSize += info.getAllocationCount() * info.getSize(); + } + Collections.sort(mLibAllocations, new Comparator<NativeAllocationInfo>() { + public int compare(NativeAllocationInfo o1, NativeAllocationInfo o2) { + return o2.getAllocationCount() * o2.getSize() - + o1.getAllocationCount() * o1.getSize(); + } + }); + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + + mDisplay = parent.getDisplay(); + + mBase = new Composite(parent, SWT.NONE); + GridLayout gl = new GridLayout(1, false); + gl.horizontalSpacing = 0; + gl.verticalSpacing = 0; + mBase.setLayout(gl); + mBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // composite for <update btn> <status> + Composite tmp = new Composite(mBase, SWT.NONE); + tmp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + tmp.setLayout(gl = new GridLayout(2, false)); + gl.marginWidth = gl.marginHeight = 0; + + mFullUpdateButton = new Button(tmp, SWT.NONE); + mFullUpdateButton.setText("Full Update"); + mFullUpdateButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mBackUpClientData = null; + mDisplayModeCombo.setEnabled(false); + mSaveButton.setEnabled(false); + emptyTables(); + // if we already have a stack call computation for this + // client + // we stop it + if (mStackCallThread != null && + mStackCallThread.getClientData() == mClientData) { + mStackCallThread.quit(); + mStackCallThread = null; + } + mLibraryAllocations.clear(); + Client client = getCurrentClient(); + if (client != null) { + client.requestNativeHeapInformation(); + } + } + }); + + mUpdateStatus = new Label(tmp, SWT.NONE); + mUpdateStatus.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // top layout for the combos and oter controls on the right. + Composite top_layout = new Composite(mBase, SWT.NONE); + top_layout.setLayout(gl = new GridLayout(4, false)); + gl.marginWidth = gl.marginHeight = 0; + + new Label(top_layout, SWT.NONE).setText("Show:"); + + mAllocDisplayCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY); + mAllocDisplayCombo.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mAllocDisplayCombo.add("All Allocations"); + mAllocDisplayCombo.add("Pre-Zygote Allocations"); + mAllocDisplayCombo.add("Zygote Child Allocations (Z)"); + mAllocDisplayCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onAllocDisplayChange(); + } + }); + mAllocDisplayCombo.select(0); + + // separator + Label separator = new Label(top_layout, SWT.SEPARATOR | SWT.VERTICAL); + GridData gd; + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + gd.verticalSpan = 2; + + mSaveButton = new Button(top_layout, SWT.PUSH); + mSaveButton.setText("Save..."); + mSaveButton.setEnabled(false); + mSaveButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mBase.getShell(), SWT.SAVE); + + fileDialog.setText("Save Allocations"); + fileDialog.setFileName("allocations.txt"); + + String fileName = fileDialog.open(); + if (fileName != null) { + saveAllocations(fileName); + } + } + }); + + /* + * TODO: either fix the diff mechanism or remove it altogether. + mDiffUpdateButton = new Button(top_layout, SWT.NONE); + mDiffUpdateButton.setText("Update && Diff"); + mDiffUpdateButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // since this is an update and diff, we need to store the + // current list + // of mallocs + mBackUpAllocations.clear(); + mBackUpAllocations.addAll(mAllocations); + mBackUpClientData = mClientData; + mBackUpTotalMemory = mClientData.getTotalNativeMemory(); + + mDisplayModeCombo.setEnabled(false); + emptyTables(); + // if we already have a stack call computation for this + // client + // we stop it + if (mStackCallThread != null && + mStackCallThread.getClientData() == mClientData) { + mStackCallThread.quit(); + mStackCallThread = null; + } + mLibraryAllocations.clear(); + Client client = getCurrentClient(); + if (client != null) { + client.requestNativeHeapInformation(); + } + } + }); + */ + + Label l = new Label(top_layout, SWT.NONE); + l.setText("Display:"); + + mDisplayModeCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY); + mDisplayModeCombo.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mDisplayModeCombo.setItems(new String[] { "Allocation List", "By Libraries" }); + mDisplayModeCombo.select(0); + mDisplayModeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + switchDisplayMode(); + } + }); + mDisplayModeCombo.setEnabled(false); + + mSymbolsButton = new Button(top_layout, SWT.PUSH); + mSymbolsButton.setText("Load Symbols"); + mSymbolsButton.setEnabled(false); + + + // create a composite that will contains the actual content composites, + // in stack mode layout. + // This top level composite contains 2 other composites. + // * one for both Allocations and Libraries mode + // * one for flat mode (which is gone for now) + + mTopStackComposite = new Composite(mBase, SWT.NONE); + mTopStackComposite.setLayout(mTopStackLayout = new StackLayout()); + mTopStackComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // create 1st and 2nd modes + createTableDisplay(mTopStackComposite); + + mTopStackLayout.topControl = mTableModeControl; + mTopStackComposite.layout(); + + setUpdateStatus(NOT_SELECTED); + + // Work in progress + // TODO add image display of native heap. + //mImage = new Label(mBase, SWT.NONE); + + mBase.pack(); + + return mBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + // TODO + } + + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) { + if (mBase.isDisposed()) + return; + + mBase.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mBase.isDisposed()) + return; + + Client client = getCurrentClient(); + + mDisplayModeCombo.setEnabled(false); + emptyTables(); + + Log.d("ddms", "NativeHeapPanel: changed " + client); + + if (client != null) { + ClientData cd = client.getClientData(); + mClientData = cd; + + // if (cd.getShowHeapUpdates()) + setUpdateStatus(ENABLED); + // else + // setUpdateStatus(NOT_ENABLED); + + initAllocationDisplay(); + + //renderBitmap(cd); + } else { + mClientData = null; + setUpdateStatus(NOT_SELECTED); + } + + mBase.pack(); + } + + /** + * Update the UI with the newly compute stack calls, unless the UI switched + * to a different client. + * + * @param cd the ClientData for which the stack call are being computed. + * @param count the current count of allocations for which the stack calls + * have been computed. + */ + @WorkerThread + public void updateAllocationStackCalls(ClientData cd, int count) { + // we have to check that the panel still shows the same clientdata than + // the thread is computing for. + if (cd == mClientData) { + + int total = mAllocations.size(); + + if (count == total) { + // we're done: do something + mDisplayModeCombo.setEnabled(true); + mSaveButton.setEnabled(true); + + mStackCallThread = null; + } else { + // work in progress, update the progress bar. +// mUiThread.setStatusLine("Computing stack call: " + count +// + "/" + total); + } + + // FIXME: attempt to only update when needed. + // Because the number of pages is not related to mAllocations.size() anymore + // due to pre-zygote/post-zygote display, update all the time. + // At some point we should remove the pages anyway, since it's getting computed + // really fast now. +// if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count +// || (count == total && mCurrentPage == mPageCount - 1)) { + try { + // get the current selection of the allocation + int index = mAllocationTable.getSelectionIndex(); + NativeAllocationInfo info = null; + + if (index != -1) { + info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData(); + } + + // empty the table + emptyTables(); + + // fill it again + fillAllocationTable(); + + // reselect + mAllocationTable.setSelection(index); + + // display detail table if needed + if (info != null) { + fillDetailTable(info); + } + } catch (SWTException e) { + if (mAllocationTable.isDisposed()) { + // looks like the table is disposed. Let's ignore it. + } else { + throw e; + } + } + + } else { + // old client still running. doesn't really matter. + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mAllocationTable); + addTableToFocusListener(mLibraryTable); + addTableToFocusListener(mLibraryAllocationTable); + addTableToFocusListener(mDetailTable); + } + + protected void onAllocDisplayChange() { + mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex(); + + // create the new list + updateAllocDisplayList(); + + updateTotalMemoryDisplay(); + + // reset the ui. + mCurrentPage = 0; + updatePageUI(); + switchDisplayMode(); + } + + private void updateAllocDisplayList() { + mTotalSize = 0; + mDisplayedAllocations.clear(); + for (NativeAllocationInfo info : mAllocations) { + if (mAllocDisplayMode == ALLOC_DISPLAY_ALL || + (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) { + mDisplayedAllocations.add(info); + mTotalSize += info.getSize() * info.getAllocationCount(); + } else { + // skip this item + continue; + } + } + + int count = mDisplayedAllocations.size(); + + mPageCount = count / DISPLAY_PER_PAGE; + + // need to add a page for the rest of the div + if ((count % DISPLAY_PER_PAGE) > 0) { + mPageCount++; + } + } + + private void updateTotalMemoryDisplay() { + switch (mAllocDisplayMode) { + case ALLOC_DISPLAY_ALL: + mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + case ALLOC_DISPLAY_PRE_ZYGOTE: + mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + case ALLOC_DISPLAY_POST_ZYGOTE: + mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + } + } + + + private void switchDisplayMode() { + switch (mDisplayModeCombo.getSelectionIndex()) { + case 0: {// allocations + mTopStackLayout.topControl = mTableModeControl; + mAllocationStackLayout.topControl = mAllocationModeTop; + mAllocationStackComposite.layout(); + mTopStackComposite.layout(); + emptyTables(); + fillAllocationTable(); + } + break; + case 1: {// libraries + mTopStackLayout.topControl = mTableModeControl; + mAllocationStackLayout.topControl = mLibraryModeTopControl; + mAllocationStackComposite.layout(); + mTopStackComposite.layout(); + emptyTables(); + fillLibraryTable(); + } + break; + } + } + + private void initAllocationDisplay() { + mAllocations.clear(); + mAllocations.addAll(mClientData.getNativeAllocationList()); + + updateAllocDisplayList(); + + // if we have a previous clientdata and it matches the current one. we + // do a diff between the new list and the old one. + if (mBackUpClientData != null && mBackUpClientData == mClientData) { + + ArrayList<NativeAllocationInfo> add = new ArrayList<NativeAllocationInfo>(); + + // we go through the list of NativeAllocationInfo in the new list and check if + // there's one with the same exact data (size, allocation, count and + // stackcall addresses) in the old list. + // if we don't find any, we add it to the "add" list + for (NativeAllocationInfo mi : mAllocations) { + boolean found = false; + for (NativeAllocationInfo old_mi : mBackUpAllocations) { + if (mi.equals(old_mi)) { + found = true; + break; + } + } + if (found == false) { + add.add(mi); + } + } + + // put the result in mAllocations + mAllocations.clear(); + mAllocations.addAll(add); + + // display the difference in memory usage. This is computed + // calculating the memory usage of the objects in mAllocations. + int count = 0; + for (NativeAllocationInfo allocInfo : mAllocations) { + count += allocInfo.getSize() * allocInfo.getAllocationCount(); + } + + mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes", + sFormatter.format(count))); + } + else { + // display the full memory usage + updateTotalMemoryDisplay(); + //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0); + } + mTotalMemoryLabel.pack(); + + // update the page ui + mDisplayModeCombo.select(0); + + mLibraryAllocations.clear(); + + // reset to first page + mCurrentPage = 0; + + // update the label + updatePageUI(); + + // now fill the allocation Table with the current page + switchDisplayMode(); + + // start the thread to compute the stack calls + if (mAllocations.size() > 0) { + mStackCallThread = new StackCallThread(mClientData); + mStackCallThread.start(); + } + } + + private void updatePageUI() { + + // set the label and pack to update the layout, otherwise + // the label will be cut off if the new size is bigger + if (mPageCount == 0) { + mPageLabel.setText("0 of 0 allocations."); + } else { + StringBuffer buffer = new StringBuffer(); + // get our starting index + int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1; + // end index, taking into account the last page can be half full + int count = mDisplayedAllocations.size(); + int end = Math.min(start + DISPLAY_PER_PAGE - 1, count); + buffer.append(sFormatter.format(start)); + buffer.append(" - "); + buffer.append(sFormatter.format(end)); + buffer.append(" of "); + buffer.append(sFormatter.format(count)); + buffer.append(" allocations."); + mPageLabel.setText(buffer.toString()); + } + + // handle the button enabled state. + mPagePreviousButton.setEnabled(mCurrentPage > 0); + // reminder: mCurrentPage starts at 0. + mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1); + + mPageLabel.pack(); + mPageUIComposite.pack(); + + } + + private void fillAllocationTable() { + // get the count + int count = mDisplayedAllocations.size(); + + // get our starting index + int start = mCurrentPage * DISPLAY_PER_PAGE; + + // loop for DISPLAY_PER_PAGE or till we reach count + int end = start + DISPLAY_PER_PAGE; + + for (int i = start; i < end && i < count; i++) { + NativeAllocationInfo info = mDisplayedAllocations.get(i); + + TableItem item = null; + + if (mAllocDisplayMode == ALLOC_DISPLAY_ALL) { + item = new TableItem(mAllocationTable, SWT.NONE); + item.setText(0, (info.isZygoteChild() ? "Z " : "") + + sFormatter.format(info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) { + item = new TableItem(mAllocationTable, SWT.NONE); + item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + } else { + // skip this item + continue; + } + + item.setData(info); + + NativeStackCallInfo bti = info.getRelevantStackCallInfo(); + if (bti != null) { + String lib = bti.getLibraryName(); + String method = bti.getMethodName(); + String source = bti.getSourceFile(); + if (lib != null) + item.setText(3, lib); + if (method != null) + item.setText(4, method); + if (source != null) + item.setText(5, source); + } + } + } + + private void fillLibraryTable() { + // fill the library table + sortAllocationsPerLibrary(); + + for (LibraryAllocations liballoc : mLibraryAllocations) { + if (liballoc != null) { + TableItem item = new TableItem(mLibraryTable, SWT.NONE); + String lib = liballoc.getLibrary(); + item.setText(0, lib != null ? lib : ""); + item.setText(1, sFormatter.format(liballoc.getSize())); + item.setText(2, sFormatter.format(liballoc.getCount())); + } + } + } + + private void fillLibraryAllocationTable() { + mLibraryAllocationTable.removeAll(); + mDetailTable.removeAll(); + int index = mLibraryTable.getSelectionIndex(); + if (index != -1) { + LibraryAllocations liballoc = mLibraryAllocations.get(index); + // start a thread that will fill table 10 at a time to keep the ui + // responsive, but first we kill the previous one if there was one + if (mFillTableThread != null) { + mFillTableThread.quit(); + } + mFillTableThread = new FillTableThread(liballoc, + liballoc.getAllocationSize()); + mFillTableThread.start(); + } + } + + public void updateLibraryAllocationTable(LibraryAllocations liballoc, + int start, int end) { + try { + if (mLibraryTable.isDisposed() == false) { + int index = mLibraryTable.getSelectionIndex(); + if (index != -1) { + LibraryAllocations newliballoc = mLibraryAllocations.get( + index); + if (newliballoc == liballoc) { + int count = liballoc.getAllocationSize(); + for (int i = start; i < end && i < count; i++) { + NativeAllocationInfo info = liballoc.getAllocation(i); + + TableItem item = new TableItem( + mLibraryAllocationTable, SWT.NONE); + item.setText(0, sFormatter.format( + info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + + NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo(); + if (stackCallInfo != null) { + item.setText(3, stackCallInfo.getMethodName()); + } + } + } else { + // we should quit the thread + if (mFillTableThread != null) { + mFillTableThread.quit(); + mFillTableThread = null; + } + } + } + } + } catch (SWTException e) { + Log.e("ddms", "error when updating the library allocation table"); + } + } + + private void fillDetailTable(final NativeAllocationInfo mi) { + mDetailTable.removeAll(); + mDetailTable.setRedraw(false); + + try { + // populate the detail Table with the back trace + Long[] addresses = mi.getStackCallAddresses(); + NativeStackCallInfo[] resolvedStackCall = mi.getResolvedStackCall(); + + if (resolvedStackCall == null) { + return; + } + + for (int i = 0 ; i < resolvedStackCall.length ; i++) { + if (addresses[i] == null || addresses[i].longValue() == 0) { + continue; + } + + long addr = addresses[i].longValue(); + NativeStackCallInfo source = resolvedStackCall[i]; + + TableItem item = new TableItem(mDetailTable, SWT.NONE); + item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$ + + String libraryName = source.getLibraryName(); + String methodName = source.getMethodName(); + String sourceFile = source.getSourceFile(); + int lineNumber = source.getLineNumber(); + + if (libraryName != null) + item.setText(1, libraryName); + if (methodName != null) + item.setText(2, methodName); + if (sourceFile != null) + item.setText(3, sourceFile); + if (lineNumber != -1) + item.setText(4, Integer.toString(lineNumber)); + } + } finally { + mDetailTable.setRedraw(true); + } + } + + /* + * Are updates enabled? + */ + private void setUpdateStatus(int status) { + switch (status) { + case NOT_SELECTED: + mUpdateStatus.setText("Select a client to see heap info"); + mAllocDisplayCombo.setEnabled(false); + mFullUpdateButton.setEnabled(false); + //mDiffUpdateButton.setEnabled(false); + break; + case NOT_ENABLED: + mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client"); + mAllocDisplayCombo.setEnabled(false); + mFullUpdateButton.setEnabled(false); + //mDiffUpdateButton.setEnabled(false); + break; + case ENABLED: + mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data"); + mAllocDisplayCombo.setEnabled(true); + mFullUpdateButton.setEnabled(true); + //mDiffUpdateButton.setEnabled(true); + break; + default: + throw new RuntimeException(); + } + + mUpdateStatus.pack(); + } + + /** + * Create the Table display. This includes a "detail" Table in the bottom + * half and 2 modes in the top half: allocation Table and + * library+allocations Tables. + * + * @param base the top parent to create the display into + */ + private void createTableDisplay(Composite base) { + final int minPanelWidth = 60; + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + // top level composite for mode 1 & 2 + mTableModeControl = new Composite(base, SWT.NONE); + GridLayout gl = new GridLayout(1, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + mTableModeControl.setLayout(gl); + mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE); + mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTotalMemoryLabel.setText("Total Memory: 0 Bytes"); + + // the top half of these modes is dynamic + + final Composite sash_composite = new Composite(mTableModeControl, + SWT.NONE); + sash_composite.setLayout(new FormLayout()); + sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // create the stacked composite + mAllocationStackComposite = new Composite(sash_composite, SWT.NONE); + mAllocationStackLayout = new StackLayout(); + mAllocationStackComposite.setLayout(mAllocationStackLayout); + mAllocationStackComposite.setLayoutData(new GridData( + GridData.FILL_BOTH)); + + // create the top half for mode 1 + createAllocationTopHalf(mAllocationStackComposite); + + // create the top half for mode 2 + createLibraryTopHalf(mAllocationStackComposite); + + final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL); + + // bottom half of these modes is the same: detail table + createDetailTable(sash_composite); + + // init value for stack + mAllocationStackLayout.topControl = mAllocationModeTop; + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(mTotalMemoryLabel, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mAllocationStackComposite.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) { + sashData.top = new FormAttachment(0, + prefs.getInt(PREFS_ALLOCATION_SASH)); + } else { + sashData.top = new FormAttachment(50, 0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mDetailTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = sash_composite.getClientArea(); + int bottom = panelRect.height - sashRect.height - minPanelWidth; + e.y = Math.max(Math.min(e.y, bottom), minPanelWidth); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + prefs.setValue(PREFS_ALLOCATION_SASH, e.y); + sash_composite.layout(); + } + } + }); + } + + private void createDetailTable(Composite base) { + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); + mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDetailTable.setHeaderVisible(true); + mDetailTable.setLinesVisible(true); + + TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT, + "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT, + "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$ + } + + private void createAllocationTopHalf(Composite b) { + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + Composite base = new Composite(b, SWT.NONE); + mAllocationModeTop = base; + GridLayout gl = new GridLayout(1, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + gl.verticalSpacing = 0; + base.setLayout(gl); + base.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // horizontal layout for memory total and pages UI + mPageUIComposite = new Composite(base, SWT.NONE); + mPageUIComposite.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_BEGINNING)); + gl = new GridLayout(3, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + gl.horizontalSpacing = 0; + mPageUIComposite.setLayout(gl); + + // Page UI + mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE); + mPagePreviousButton.setText("<"); + mPagePreviousButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mCurrentPage--; + updatePageUI(); + emptyTables(); + fillAllocationTable(); + } + }); + + mPageNextButton = new Button(mPageUIComposite, SWT.NONE); + mPageNextButton.setText(">"); + mPageNextButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mCurrentPage++; + updatePageUI(); + emptyTables(); + fillAllocationTable(); + } + }); + + mPageLabel = new Label(mPageUIComposite, SWT.NONE); + mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + updatePageUI(); + + mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); + mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mAllocationTable.setHeaderVisible(true); + mAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT, + "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT, + "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT, + "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$ + + mAllocationTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection index + int index = mAllocationTable.getSelectionIndex(); + TableItem item = mAllocationTable.getItem(index); + if (item != null && item.getData() instanceof NativeAllocationInfo) { + fillDetailTable((NativeAllocationInfo)item.getData()); + } + } + }); + } + + private void createLibraryTopHalf(Composite base) { + final int minPanelWidth = 60; + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + // create a composite that'll contain 2 tables horizontally + final Composite top = new Composite(base, SWT.NONE); + mLibraryModeTopControl = top; + top.setLayout(new FormLayout()); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // first table: library + mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mLibraryTable.setHeaderVisible(true); + mLibraryTable.setLinesVisible(true); + + TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT, + "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT, + "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$ + + mLibraryTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + fillLibraryAllocationTable(); + } + }); + + final Sash sash = new Sash(top, SWT.VERTICAL); + + // 2nd table: allocation per library + mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mLibraryAllocationTable.setHeaderVisible(true); + mLibraryAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn(mLibraryAllocationTable, "Total", + SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Count", + SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Size", + SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Method", + SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$ + + mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the index of the selection in the library table + int index1 = mLibraryTable.getSelectionIndex(); + // get the index in the library allocation table + int index2 = mLibraryAllocationTable.getSelectionIndex(); + // get the MallocInfo object + LibraryAllocations liballoc = mLibraryAllocations.get(index1); + NativeAllocationInfo info = liballoc.getAllocation(index2); + fillDetailTable(info); + } + }); + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(sash, 0); + mLibraryTable.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) { + sashData.left = new FormAttachment(0, + prefs.getInt(PREFS_LIBRARY_SASH)); + } else { + sashData.left = new FormAttachment(50, 0); + } + sashData.bottom = new FormAttachment(100, 0); + sashData.top = new FormAttachment(0, 0); // 50% across + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(sash, 0); + data.right = new FormAttachment(100, 0); + mLibraryAllocationTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = top.getClientArea(); + int right = panelRect.width - sashRect.width - minPanelWidth; + e.x = Math.max(Math.min(e.x, right), minPanelWidth); + if (e.x != sashRect.x) { + sashData.left = new FormAttachment(0, e.x); + prefs.setValue(PREFS_LIBRARY_SASH, e.y); + top.layout(); + } + } + }); + } + + private void emptyTables() { + mAllocationTable.removeAll(); + mLibraryTable.removeAll(); + mLibraryAllocationTable.removeAll(); + mDetailTable.removeAll(); + } + + private void sortAllocationsPerLibrary() { + if (mClientData != null) { + mLibraryAllocations.clear(); + + // create a hash map of LibraryAllocations to access aggregate + // objects already created + HashMap<String, LibraryAllocations> libcache = + new HashMap<String, LibraryAllocations>(); + + // get the allocation count + int count = mDisplayedAllocations.size(); + for (int i = 0; i < count; i++) { + NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i); + + NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo(); + if (stackCallInfo != null) { + String libraryName = stackCallInfo.getLibraryName(); + LibraryAllocations liballoc = libcache.get(libraryName); + if (liballoc == null) { + // didn't find a library allocation object already + // created so we create one + liballoc = new LibraryAllocations(libraryName); + // add it to the cache + libcache.put(libraryName, liballoc); + // add it to the list + mLibraryAllocations.add(liballoc); + } + // add the MallocInfo object to it. + liballoc.addAllocation(allocInfo); + } + } + // now that the list is created, we need to compute the size and + // sort it by size. This will also sort the MallocInfo objects + // inside each LibraryAllocation objects. + for (LibraryAllocations liballoc : mLibraryAllocations) { + liballoc.computeAllocationSizeAndCount(); + } + + // now we sort it + Collections.sort(mLibraryAllocations, + new Comparator<LibraryAllocations>() { + public int compare(LibraryAllocations o1, + LibraryAllocations o2) { + return o2.getSize() - o1.getSize(); + } + }); + } + } + + private void renderBitmap(ClientData cd) { + byte[] pixData; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false) { + // no change, we return. + return; + } + + pixData = getSerializedData(); + + ImageData id = createLinearHeapImage(pixData, 200, mMapPalette); + Image image = new Image(mBase.getDisplay(), id); + mImage.setImage(image); + mImage.pack(true); + } + } + + /* + * Create color palette for map. Set up titles for legend. + */ + private static PaletteData createPalette() { + RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; + colors[0] + = new RGB(192, 192, 192); // non-heap pixels are gray + mMapLegend[0] + = "(heap expansion area)"; + + colors[1] + = new RGB(0, 0, 0); // free chunks are black + mMapLegend[1] + = "free"; + + colors[HeapSegmentElement.KIND_OBJECT + 2] + = new RGB(0, 0, 255); // objects are blue + mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] + = "data object"; + + colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = new RGB(0, 255, 0); // class objects are green + mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = "class object"; + + colors[HeapSegmentElement.KIND_ARRAY_1 + 2] + = new RGB(255, 0, 0); // byte/bool arrays are red + mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] + = "1-byte array (byte[], boolean[])"; + + colors[HeapSegmentElement.KIND_ARRAY_2 + 2] + = new RGB(255, 128, 0); // short/char arrays are orange + mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] + = "2-byte array (short[], char[])"; + + colors[HeapSegmentElement.KIND_ARRAY_4 + 2] + = new RGB(255, 255, 0); // obj/int/float arrays are yellow + mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] + = "4-byte array (object[], int[], float[])"; + + colors[HeapSegmentElement.KIND_ARRAY_8 + 2] + = new RGB(255, 128, 128); // long/double arrays are pink + mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] + = "8-byte array (long[], double[])"; + + colors[HeapSegmentElement.KIND_UNKNOWN + 2] + = new RGB(255, 0, 255); // unknown objects are cyan + mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] + = "unknown object"; + + colors[HeapSegmentElement.KIND_NATIVE + 2] + = new RGB(64, 64, 64); // native objects are dark gray + mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] + = "non-Java object"; + + return new PaletteData(colors); + } + + private void saveAllocations(String fileName) { + try { + PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); + + for (NativeAllocationInfo alloc : mAllocations) { + out.println(alloc.toString()); + } + out.close(); + } catch (IOException e) { + Log.e("Native", e); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java new file mode 100644 index 0000000..d910cc7 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java @@ -0,0 +1,49 @@ +/* + * 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 org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + + +/** + * Base class for our information panels. + */ +public abstract class Panel { + + public final Control createPanel(Composite parent) { + Control panelControl = createControl(parent); + + postCreation(); + + return panelControl; + } + + protected abstract void postCreation(); + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + protected abstract Control createControl(Composite parent); + + /** + * Sets the focus to the proper control inside the panel. + */ + public abstract void setFocus(); +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java new file mode 100644 index 0000000..533372e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java @@ -0,0 +1,73 @@ +/* + * 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 org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.swt.widgets.Composite; + +/** + * Edit an integer field, validating it as a port number. + */ +public class PortFieldEditor extends IntegerFieldEditor { + + public boolean mRecursiveCheck = false; + + public PortFieldEditor(String name, String label, Composite parent) { + super(name, label, parent); + setValidateStrategy(VALIDATE_ON_KEY_STROKE); + } + + /* + * Get the current value of the field, as an integer. + */ + public int getCurrentValue() { + int val; + try { + val = Integer.parseInt(getStringValue()); + } + catch (NumberFormatException nfe) { + val = -1; + } + return val; + } + + /* + * Check the validity of the field. + */ + @Override + protected boolean checkState() { + if (super.checkState() == false) { + return false; + } + //Log.i("ddms", "check state " + getStringValue()); + boolean err = false; + int val = getCurrentValue(); + if (val < 1024 || val > 32767) { + setErrorMessage("Port must be between 1024 and 32767"); + err = true; + } else { + setErrorMessage(null); + err = false; + } + showErrorMessage(); + return !err; + } + + protected void updateCheckState(PortFieldEditor pfe) { + pfe.refreshValidState(); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java new file mode 100644 index 0000000..dad54dc --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java @@ -0,0 +1,256 @@ +/* + * 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 com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.RawImage; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +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.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.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.io.IOException; + + +/** + * Gather a screen shot from the device and save it to a file. + */ +public class ScreenShotDialog extends Dialog { + + private Label mBusyLabel; + private Label mImageLabel; + private Button mSave; + private Device mDevice; + + + /** + * Create with default style. + */ + public ScreenShotDialog(Shell parent) { + this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + } + + /** + * Create with app-defined style. + */ + public ScreenShotDialog(Shell parent, int style) { + super(parent, style); + } + + /** + * Prepare and display the dialog. + * @param device The {@link Device} from which to get the screenshot. + */ + public void open(Device device) { + mDevice = device; + + Shell parent = getParent(); + Shell shell = new Shell(parent, getStyle()); + shell.setText("Device Screen Capture"); + + createContents(shell); + shell.pack(); + shell.open(); + + updateDeviceImage(shell); + + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + } + + /* + * Create the screen capture dialog contents. + */ + private void createContents(final Shell shell) { + GridData data; + + shell.setLayout(new GridLayout(3, true)); + + // title/"capturing" label + mBusyLabel = new Label(shell, SWT.NONE); + mBusyLabel.setText("Preparing..."); + data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); + data.horizontalSpan = 3; + mBusyLabel.setLayoutData(data); + + // space for the image + mImageLabel = new Label(shell, SWT.BORDER); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.horizontalSpan = 3; + mImageLabel.setLayoutData(data); + Display display = shell.getDisplay(); + mImageLabel.setImage(ImageHelper.createPlaceHolderArt(display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE))); + + // "refresh" button + Button refresh = new Button(shell, SWT.PUSH); + refresh.setText("Refresh"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + refresh.setLayoutData(data); + refresh.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateDeviceImage(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); + } + }); + + // "done" button + Button done = new Button(shell, SWT.PUSH); + done.setText("Done"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + done.setLayoutData(data); + done.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + shell.close(); + } + }); + + shell.setDefaultButton(done); + } + + /* + * Capture a new image from the device. + */ + private void updateDeviceImage(Shell shell) { + mBusyLabel.setText("Capturing..."); // no effect + + shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT)); + + Image image = getDeviceImage(); + if (image == null) { + Display display = shell.getDisplay(); + image = ImageHelper.createPlaceHolderArt(display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE)); + mSave.setEnabled(false); + mBusyLabel.setText("Screen not available"); + } else { + 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)); + } + + /* + * Grab an image from an ADB-connected device. + */ + private Image getDeviceImage() { + RawImage rawImage; + + try { + rawImage = mDevice.getScreenshot(); + } + catch (IOException ioe) { + Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage()); + return null; + } + + // device/adb not available? + if (rawImage == null) + return null; + + // convert raw data to an Image + assert rawImage.bpp == 16; + PaletteData palette = new PaletteData(0xf800, 0x07e0, 0x001f); + ImageData imageData = new ImageData(rawImage.width, rawImage.height, + rawImage.bpp, palette, 1, rawImage.data); + + return new Image(getParent().getDisplay(), imageData); + } + + /* + * Prompt the user to save the image to disk. + */ + private void saveImage(Shell shell) { + FileDialog dlg = new FileDialog(shell, SWT.SAVE); + String fileName; + + dlg.setText("Save image..."); + dlg.setFileName("device.png"); + dlg.setFilterPath(DdmUiPreferences.getStore().getString("lastImageSaveDir")); + dlg.setFilterNames(new String[] { + "PNG Files (*.png)" + }); + dlg.setFilterExtensions(new String[] { + "*.png" //$NON-NLS-1$ + }); + + fileName = dlg.open(); + if (fileName != null) { + DdmUiPreferences.getStore().setValue("lastImageSaveDir", dlg.getFilterPath()); + + Log.i("ddms", "Saving image to " + fileName); + ImageData imageData = mImageLabel.getImage().getImageData(); + + try { + WritePng.savePng(fileName, imageData); + } + catch (IOException ioe) { + Log.w("ddms", "Unable to save " + fileName + ": " + ioe); + } + + if (false) { + ImageLoader loader = new ImageLoader(); + loader.data = new ImageData[] { imageData }; + // PNG writing not available until 3.3? See bug at: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=24697 + // GIF writing only works for 8 bits + // JPEG uses lossy compression + // BMP has screwed-up colors + loader.save(fileName, SWT.IMAGE_JPEG); + } + } + } + +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java new file mode 100644 index 0000000..4b5fe56 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java @@ -0,0 +1,78 @@ +/* + * 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 com.android.ddmlib.Client; +import com.android.ddmlib.Device; + +/** + * A Panel that requires {@link Device}/{@link Client} selection notifications. + */ +public abstract class SelectionDependentPanel extends Panel { + private Device mCurrentDevice = null; + private Client mCurrentClient = null; + + /** + * Returns the current {@link Device}. + * @return the current device or null if none are selected. + */ + protected final Device getCurrentDevice() { + return mCurrentDevice; + } + + /** + * Returns the current {@link Client}. + * @return the current client or null if none are selected. + */ + protected final Client getCurrentClient() { + return mCurrentClient; + } + + /** + * Sent when a new device is selected. + * @param selectedDevice the selected device. + */ + public final void deviceSelected(Device selectedDevice) { + if (selectedDevice != mCurrentDevice) { + mCurrentDevice = selectedDevice; + deviceSelected(); + } + } + + /** + * Sent when a new client is selected. + * @param selectedClient the selected client. + */ + public final void clientSelected(Client selectedClient) { + if (selectedClient != mCurrentClient) { + mCurrentClient = selectedClient; + clientSelected(); + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + public abstract void deviceSelected(); + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + public abstract void clientSelected(); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java new file mode 100644 index 0000000..3358962 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2008 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 com.android.ddmlib.Client; +import com.android.ddmlib.IStackTraceInfo; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Table; + +/** + * Stack Trace Panel. + * <p/>This is not a panel in the regular sense. Instead this is just an object around the creation + * and management of a Stack Trace display. + * <p/>UI creation is done through + * {@link #createPanel(Composite, String, String, String, String, String, IPreferenceStore)}. + * + */ +public final class StackTracePanel { + + private static ISourceRevealer sSourceRevealer; + + private Table mStackTraceTable; + private TableViewer mStackTraceViewer; + + private Client mCurrentClient; + + + /** + * Content Provider to display the stack trace of a thread. + * Expected input is a {@link IStackTraceInfo} object. + */ + private static class StackTraceContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof IStackTraceInfo) { + // getElement cannot return null, so we return an empty array + // if there's no stack trace + StackTraceElement trace[] = ((IStackTraceInfo)inputElement).getStackTrace(); + if (trace != null) { + return trace; + } + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + + /** + * A Label Provider to use with {@link StackTraceContentProvider}. It expects the elements to be + * of type {@link StackTraceElement}. + */ + private static class StackTraceLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof StackTraceElement) { + StackTraceElement traceElement = (StackTraceElement)element; + switch (columnIndex) { + case 0: + return traceElement.getClassName(); + case 1: + return traceElement.getMethodName(); + case 2: + return traceElement.getFileName(); + case 3: + return Integer.toString(traceElement.getLineNumber()); + case 4: + return Boolean.toString(traceElement.isNativeMethod()); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Classes which implement this interface provide a method that is able to reveal a method + * in a source editor + */ + public interface ISourceRevealer { + /** + * Sent to reveal a particular line in a source editor + * @param applicationName the name of the application running the source. + * @param className the fully qualified class name + * @param line the line to reveal + */ + public void reveal(String applicationName, String className, int line); + } + + + /** + * Sets the {@link ISourceRevealer} object able to reveal source code in a source editor. + * @param revealer + */ + public static void setSourceRevealer(ISourceRevealer revealer) { + sSourceRevealer = revealer; + } + + /** + * Creates the controls for the StrackTrace display. + * <p/>This method will set the parent {@link Composite} to use a {@link GridLayout} with + * 2 columns. + * @param parent the parent composite. + * @param prefs_stack_col_class + * @param prefs_stack_col_method + * @param prefs_stack_col_file + * @param prefs_stack_col_line + * @param prefs_stack_col_native + * @param store + */ + public Table createPanel(Composite parent, String prefs_stack_col_class, + String prefs_stack_col_method, String prefs_stack_col_file, String prefs_stack_col_line, + String prefs_stack_col_native, IPreferenceStore store) { + + mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mStackTraceTable.setHeaderVisible(true); + mStackTraceTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mStackTraceTable, + "Class", + SWT.LEFT, + "SomeLongClassName", //$NON-NLS-1$ + prefs_stack_col_class, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Method", + SWT.LEFT, + "someLongMethod", //$NON-NLS-1$ + prefs_stack_col_method, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "File", + SWT.LEFT, + "android/somepackage/someotherpackage/somefile.class", //$NON-NLS-1$ + prefs_stack_col_file, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Line", + SWT.RIGHT, + "99999", //$NON-NLS-1$ + prefs_stack_col_line, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Native", + SWT.LEFT, + "Native", //$NON-NLS-1$ + prefs_stack_col_native, store); + + mStackTraceViewer = new TableViewer(mStackTraceTable); + mStackTraceViewer.setContentProvider(new StackTraceContentProvider()); + mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider()); + + mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + if (sSourceRevealer != null && mCurrentClient != null) { + // get the selected stack trace element + ISelection selection = mStackTraceViewer.getSelection(); + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof StackTraceElement) { + StackTraceElement traceElement = (StackTraceElement)object; + + if (traceElement.isNativeMethod() == false) { + sSourceRevealer.reveal( + mCurrentClient.getClientData().getClientDescription(), + traceElement.getClassName(), + traceElement.getLineNumber()); + } + } + } + } + } + }); + + return mStackTraceTable; + } + + /** + * Sets the input for the {@link TableViewer}. + * @param input the {@link IStackTraceInfo} that will provide the viewer with the list of + * {@link StackTraceElement} + */ + public void setViewerInput(IStackTraceInfo input) { + mStackTraceViewer.setInput(input); + mStackTraceViewer.refresh(); + } + + /** + * Sets the current client running the stack trace. + * @param currentClient the {@link Client}. + */ + public void setCurrentClient(Client currentClient) { + mCurrentClient = currentClient; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java new file mode 100644 index 0000000..8ef237c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java @@ -0,0 +1,582 @@ +/* + * Copyright (C) 2008 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 com.android.ddmlib.Client; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.data.general.DefaultPieDataset; +import org.jfree.experimental.chart.swt.ChartComposite; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Displays system information graphs obtained from a bugreport file or device. + */ +public class SysinfoPanel extends TablePanel implements IShellOutputReceiver { + + // UI components + private Label mLabel; + private Button mFetchButton; + private Combo mDisplayMode; + + private DefaultPieDataset mDataset; + + // The bugreport file to process + private File mDataFile; + + // To get output from adb commands + private FileOutputStream mTempStream; + + // Selects the current display: MODE_CPU, etc. + private int mMode = 0; + + private static final int MODE_CPU = 0; + private static final int MODE_ALARM = 1; + private static final int MODE_WAKELOCK = 2; + private static final int MODE_MEMINFO = 3; + private static final int MODE_SYNC = 4; + + // argument to dumpsys; section in the bugreport holding the data + private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm", + "batteryinfo", "MEMORY INFO", "content"}; + + private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo", + "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank", + "dumpsys content"}; + + private static final String CAPTIONS[] = {"CPU load", "Alarms", + "Wakelocks", "Memory usage", "Sync"}; + + /** + * Generates the dataset to display. + * + * @param file The bugreport file to process. + */ + public void generateDataset(File file) { + mDataset.clear(); + mLabel.setText(""); + if (file == null) { + return; + } + try { + BufferedReader br = getBugreportReader(file); + if (mMode == MODE_CPU) { + readCpuDataset(br); + } else if (mMode == MODE_ALARM) { + readAlarmDataset(br); + } else if (mMode == MODE_WAKELOCK) { + readWakelockDataset(br); + } else if (mMode == MODE_MEMINFO) { + readMeminfoDataset(br); + } else if (mMode == MODE_SYNC) { + readSyncDataset(br); + } + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Sent when a new device is selected. The new device can be accessed with + * {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + if (getCurrentDevice() != null) { + mFetchButton.setEnabled(true); + loadFromDevice(); + } else { + mFetchButton.setEnabled(false); + } + } + + /** + * Sent when a new client is selected. The new client can be accessed with + * {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mDisplayMode.setFocus(); + } + + /** + * Fetches a new bugreport from the device and updates the display. + * Fetching is asynchronous. See also addOutput, flush, and isCancelled. + */ + private void loadFromDevice() { + try { + initShellOutputBuffer(); + if (mMode == MODE_MEMINFO) { + // Hack to add bugreport-style section header for meminfo + mTempStream.write("------ MEMORY INFO ------\n".getBytes()); + } + getCurrentDevice().executeShellCommand( + DUMP_COMMAND[mMode], this); + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Initializes temporary output file for executeShellCommand(). + * + * @throws IOException on file error + */ + void initShellOutputBuffer() throws IOException { + mDataFile = File.createTempFile("ddmsfile", ".txt"); + mDataFile.deleteOnExit(); + mTempStream = new FileOutputStream(mDataFile); + } + + /** + * Adds output to the temp file. IShellOutputReceiver method. Called by + * executeShellCommand(). + */ + public void addOutput(byte[] data, int offset, int length) { + try { + mTempStream.write(data, offset, length); + } + catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Processes output from shell command. IShellOutputReceiver method. The + * output is passed to generateDataset(). Called by executeShellCommand() on + * completion. + */ + public void flush() { + if (mTempStream != null) { + try { + mTempStream.close(); + generateDataset(mDataFile); + mTempStream = null; + mDataFile = null; + } catch (IOException e) { + Log.e("DDMS", e); + } + } + } + + /** + * IShellOutputReceiver method. + * + * @return false - don't cancel + */ + public boolean isCancelled() { + return false; + } + + /** + * Create our controls for the UI panel. + */ + @Override + protected Control createControl(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, false)); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Composite buttons = new Composite(top, SWT.NONE); + buttons.setLayout(new RowLayout()); + + mDisplayMode = new Combo(buttons, SWT.PUSH); + for (String mode : CAPTIONS) { + mDisplayMode.add(mode); + } + mDisplayMode.select(mMode); + mDisplayMode.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mMode = mDisplayMode.getSelectionIndex(); + if (mDataFile != null) { + generateDataset(mDataFile); + } else if (getCurrentDevice() != null) { + loadFromDevice(); + } + } + }); + + final Button loadButton = new Button(buttons, SWT.PUSH); + loadButton.setText("Load from File"); + loadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(loadButton.getShell(), + SWT.OPEN); + fileDialog.setText("Load bugreport"); + String filename = fileDialog.open(); + if (filename != null) { + mDataFile = new File(filename); + generateDataset(mDataFile); + } + } + }); + + mFetchButton = new Button(buttons, SWT.PUSH); + mFetchButton.setText("Update from Device"); + mFetchButton.setEnabled(false); + mFetchButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + loadFromDevice(); + } + }); + + mLabel = new Label(top, SWT.NONE); + mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mDataset = new DefaultPieDataset(); + JFreeChart chart = ChartFactory.createPieChart("", mDataset, false + /* legend */, true/* tooltips */, false /* urls */); + + ChartComposite chartComposite = new ChartComposite(top, + SWT.BORDER, chart, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, + // max draw width. We don't want it to zoom, so we put a big number + 3000, + // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + false, // zoom + true); + chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + return top; + } + + public void clientChanged(final Client client, int changeMask) { + // Don't care + } + + /** + * Helper to open a bugreport and skip to the specified section. + * + * @param file File to open + * @return Reader to bugreport file + * @throws java.io.IOException on file error + */ + private BufferedReader getBugreportReader(File file) throws + IOException { + BufferedReader br = new BufferedReader(new FileReader(file)); + // Skip over the unwanted bugreport sections + while (true) { + String line = br.readLine(); + if (line == null) { + Log.d("DDMS", "Service not found " + line); + break; + } + if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) && + line.indexOf(BUGREPORT_SECTION[mMode]) > 0) { + break; + } + } + return br; + } + + /** + * Parse the time string generated by BatteryStats. + * A typical new-format string is "11d 13h 45m 39s 999ms". + * A typical old-format string is "12.3 sec". + * @return time in ms + */ + private static long parseTimeMs(String s) { + long total = 0; + // Matches a single component e.g. "12.3 sec" or "45ms" + Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)"); + Matcher m = p.matcher(s); + while (m.find()) { + String label = m.group(2); + if ("sec".equals(label)) { + // Backwards compatibility with old time format + total += (long) (Double.parseDouble(m.group(1)) * 1000); + continue; + } + long value = Integer.parseInt(m.group(1)); + if ("d".equals(label)) { + total += value * 24 * 60 * 60 * 1000; + } else if ("h".equals(label)) { + total += value * 60 * 60 * 1000; + } else if ("m".equals(label)) { + total += value * 60 * 1000; + } else if ("s".equals(label)) { + total += value * 1000; + } else if ("ms".equals(label)) { + total += value; + } + } + return total; + } + /** + * Processes wakelock information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readWakelockDataset(BufferedReader br) throws IOException { + Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial"); + Pattern totalPattern = Pattern.compile("Total: (.+) uptime"); + double total = 0; + boolean inCurrent = false; + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith("Current Battery Usage Statistics")) { + inCurrent = true; + } else if (inCurrent) { + Matcher m = lockPattern.matcher(line); + if (m.find()) { + double value = parseTimeMs(m.group(2)) / 1000.; + mDataset.setValue(m.group(1), value); + total -= value; + } else { + m = totalPattern.matcher(line); + if (m.find()) { + total += parseTimeMs(m.group(1)) / 1000.; + } + } + } + } + if (total > 0) { + mDataset.setValue("Unlocked", total); + } + } + + /** + * Processes alarm information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readAlarmDataset(BufferedReader br) throws IOException { + Pattern pattern = Pattern + .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags"); + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + Matcher m = pattern.matcher(line); + if (m.find()) { + long count = Long.parseLong(m.group(1)); + String name = m.group(2); + mDataset.setValue(name, count); + } + } + } + + /** + * Processes cpu load information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readCpuDataset(BufferedReader br) throws IOException { + Pattern pattern = Pattern + .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel"); + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith("Load:")) { + mLabel.setText(line); + continue; + } + Matcher m = pattern.matcher(line); + if (m.find()) { + String name = m.group(1); + long both = Long.parseLong(m.group(2)); + long user = Long.parseLong(m.group(3)); + long kernel = Long.parseLong(m.group(4)); + if ("TOTAL".equals(name)) { + if (both < 100) { + mDataset.setValue("Idle", (100 - both)); + } + } else { + // Try to make graphs more useful even with rounding; + // log often has 0% user + 0% kernel = 1% total + // We arbitrarily give extra to kernel + if (user > 0) { + mDataset.setValue(name + " (user)", user); + } + if (kernel > 0) { + mDataset.setValue(name + " (kernel)" , both - user); + } + if (user == 0 && kernel == 0 && both > 0) { + mDataset.setValue(name, both); + } + } + } + } + } + + /** + * Processes meminfo information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readMeminfoDataset(BufferedReader br) throws IOException { + Pattern valuePattern = Pattern.compile("(\\d+) kB"); + long total = 0; + long other = 0; + mLabel.setText("PSS in kB"); + + // Scan meminfo + while (true) { + String line = br.readLine(); + if (line == null) { + // End of file + break; + } + Matcher m = valuePattern.matcher(line); + if (m.find()) { + long kb = Long.parseLong(m.group(1)); + if (line.startsWith("MemTotal")) { + total = kb; + } else if (line.startsWith("MemFree")) { + mDataset.setValue("Free", kb); + total -= kb; + } else if (line.startsWith("Slab")) { + mDataset.setValue("Slab", kb); + total -= kb; + } else if (line.startsWith("PageTables")) { + mDataset.setValue("PageTables", kb); + total -= kb; + } else if (line.startsWith("Buffers") && kb > 0) { + mDataset.setValue("Buffers", kb); + total -= kb; + } else if (line.startsWith("Inactive")) { + mDataset.setValue("Inactive", kb); + total -= kb; + } else if (line.startsWith("MemFree")) { + mDataset.setValue("Free", kb); + total -= kb; + } + } else { + break; + } + } + // Scan procrank + while (true) { + String line = br.readLine(); + if (line == null) { + break; + } + if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) { + // procrank header + continue; + } + if (line.indexOf("----") >= 0) { + //end of procrank section + break; + } + // Extract pss field from procrank output + long pss = Long.parseLong(line.substring(23, 31).trim()); + String cmdline = line.substring(43).trim().replace("/system/bin/", ""); + // Arbitrary minimum size to display + if (pss > 2000) { + mDataset.setValue(cmdline, pss); + } else { + other += pss; + } + total -= pss; + } + mDataset.setValue("Other", other); + mDataset.setValue("Unknown", total); + } + + /** + * Processes sync information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readSyncDataset(BufferedReader br) throws IOException { + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith(" |") && line.length() > 70) { + String authority = line.substring(3, 18).trim(); + String duration = line.substring(61, 70).trim(); + // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime) + String durParts[] = duration.split(":"); + if (durParts.length == 2) { + long dur = Long.parseLong(durParts[0]) * 60 + Long + .parseLong(durParts[1]); + mDataset.setValue(authority, dur); + } else if (duration.length() == 3) { + long dur = Long.parseLong(durParts[0]) * 3600 + + Long.parseLong(durParts[1]) * 60 + Long + .parseLong(durParts[2]); + mDataset.setValue(authority, dur); + } + } + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java new file mode 100644 index 0000000..f8d457e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java @@ -0,0 +1,149 @@ +/* + * 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 org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +/** + * Utility class to help using Table objects. + * + */ +public final class TableHelper { + /** + * Create a TableColumn with the specified parameters. If a + * <code>PreferenceStore</code> object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + * @return The TableColumn object that was created + */ + public static TableColumn createTableColumn(Table parent, String header, + int style, String sample_text, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TableColumn col = new TableColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TableColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + + return col; + } + + /** + * Create a TreeColumn with the specified parameters. If a + * <code>PreferenceStore</code> object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + */ + public static void createTreeColumn(Tree parent, String header, int style, + String sample_text, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TreeColumn col = new TreeColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TreeColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java new file mode 100644 index 0000000..b037193 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java @@ -0,0 +1,128 @@ +/* + * 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 com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; + +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.util.Arrays; + +/** + * Base class for panel containing Table that need to support copy-paste-selectAll + */ +public abstract class TablePanel extends ClientDisplayPanel { + private ITableFocusListener mGlobalListener; + + /** + * Sets a TableFocusListener which will be notified when one of the tables + * gets or loses focus. + * + * @param listener + */ + public final void setTableFocusListener(ITableFocusListener listener) { + // record the global listener, to make sure table created after + // this call will still be setup. + mGlobalListener = listener; + + setTableFocusListener(); + } + + /** + * Sets up the Table of object of the panel to work with the global listener.<br> + * Default implementation does nothing. + */ + protected void setTableFocusListener() { + + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus. + * + * @param table the Table object. + * @param colStart + * @param colEnd + */ + protected final void addTableToFocusListener(final Table table, + final int colStart, final int colEnd) { + // create the activator for this table + final IFocusedTableActivator activator = new IFocusedTableActivator() { + public void copy(Clipboard clipboard) { + int[] selection = table.getSelectionIndices(); + + // we need to sort the items to be sure. + Arrays.sort(selection); + + // all lines must be concatenated. + StringBuilder sb = new StringBuilder(); + + // loop on the selection and output the file. + for (int i : selection) { + TableItem item = table.getItem(i); + for (int c = colStart ; c <= colEnd ; c++) { + sb.append(item.getText(c)); + sb.append('\t'); + } + sb.append('\n'); + } + + // now add that to the clipboard if the string has content + String data = sb.toString(); + if (data != null || data.length() > 0) { + clipboard.setContents( + new Object[] { data }, + new Transfer[] { TextTransfer.getInstance() }); + } + } + + public void selectAll() { + table.selectAll(); + } + }; + + // add the focus listener on the table to notify the global listener + table.addFocusListener(new FocusListener() { + public void focusGained(FocusEvent e) { + mGlobalListener.focusGained(activator); + } + + public void focusLost(FocusEvent e) { + mGlobalListener.focusLost(activator); + } + }); + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus.<br> + * When the copy method is invoked, all columns are put in the clipboard, separated + * by tabs + * + * @param table the Table object. + */ + protected final void addTableToFocusListener(final Table table) { + addTableToFocusListener(table, 0, table.getColumnCount()-1); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java new file mode 100644 index 0000000..a034063 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java @@ -0,0 +1,572 @@ +/* + * 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 com.android.ddmlib.Client; +import com.android.ddmlib.ThreadInfo; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Table; + +import java.util.Date; + +/** + * Base class for our information panels. + */ +public class ThreadPanel extends TablePanel { + + private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$ + + private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$ + + private static final String PREFS_STACK_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$NON-NLS-1$ + + private Display mDisplay; + private Composite mBase; + private Label mNotEnabled; + private Label mNotSelected; + + private Composite mThreadBase; + private Table mThreadTable; + private TableViewer mThreadViewer; + + private Composite mStackTraceBase; + private Button mRefreshStackTraceButton; + private Label mStackTraceTimeLabel; + private StackTracePanel mStackTracePanel; + private Table mStackTraceTable; + + /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */ + private boolean mMustStopRecurringThreadUpdate = false; + /** Flag to tell the recurring thread update to stop running */ + private boolean mRecurringThreadUpdateRunning = false; + + private Object mLock = new Object(); + + private static final String[] THREAD_STATUS = { + "zombie", "running", "timed-wait", "monitor", + "wait", "init", "start", "native", "vmwait" + }; + + /** + * Content Provider to display the threads of a client. + * Expected input is a {@link Client} object. + */ + private static class ThreadContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Client) { + return ((Client)inputElement).getClientData().getThreads(); + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + + /** + * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be + * of type {@link ThreadInfo}. + */ + private static class ThreadLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof ThreadInfo) { + ThreadInfo thread = (ThreadInfo)element; + switch (columnIndex) { + case 0: + return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$ + String.valueOf(thread.getThreadId()); + case 1: + return String.valueOf(thread.getTid()); + case 2: + if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length) + return THREAD_STATUS[thread.getStatus()]; + return "unknown"; + case 3: + return String.valueOf(thread.getUtime()); + case 4: + return String.valueOf(thread.getStime()); + case 5: + return thread.getThreadName(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + final IPreferenceStore store = DdmUiPreferences.getStore(); + + mBase = new Composite(parent, SWT.NONE); + mBase.setLayout(new StackLayout()); + + // UI for thread not enabled + mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP); + mNotEnabled.setText("Thread updates not enabled for selected client\n" + + "(use toolbar button to enable)"); + + // UI for not client selected + mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP); + mNotSelected.setText("no client is selected"); + + // base composite for selected client with enabled thread update. + mThreadBase = new Composite(mBase, SWT.NONE); + mThreadBase.setLayout(new FormLayout()); + + // table above the sash + mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION); + mThreadTable.setHeaderVisible(true); + mThreadTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mThreadTable, + "ID", + SWT.RIGHT, + "888", //$NON-NLS-1$ + PREFS_THREAD_COL_ID, store); + + TableHelper.createTableColumn( + mThreadTable, + "Tid", + SWT.RIGHT, + "88888", //$NON-NLS-1$ + PREFS_THREAD_COL_TID, store); + + TableHelper.createTableColumn( + mThreadTable, + "Status", + SWT.LEFT, + "timed-wait", //$NON-NLS-1$ + PREFS_THREAD_COL_STATUS, store); + + TableHelper.createTableColumn( + mThreadTable, + "utime", + SWT.RIGHT, + "utime", //$NON-NLS-1$ + PREFS_THREAD_COL_UTIME, store); + + TableHelper.createTableColumn( + mThreadTable, + "stime", + SWT.RIGHT, + "utime", //$NON-NLS-1$ + PREFS_THREAD_COL_STIME, store); + + TableHelper.createTableColumn( + mThreadTable, + "Name", + SWT.LEFT, + "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$ + PREFS_THREAD_COL_NAME, store); + + mThreadViewer = new TableViewer(mThreadTable); + mThreadViewer.setContentProvider(new ThreadContentProvider()); + mThreadViewer.setLabelProvider(new ThreadLabelProvider()); + + mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ThreadInfo selectedThread = getThreadSelection(event.getSelection()); + updateThreadStackTrace(selectedThread); + } + }); + mThreadViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + ThreadInfo selectedThread = getThreadSelection(event.getSelection()); + if (selectedThread != null) { + Client client = (Client)mThreadViewer.getInput(); + + if (client != null) { + client.requestThreadStackTrace(selectedThread.getThreadId()); + } + } + } + }); + + // the separating sash + final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL); + Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + sash.setBackground(darkGray); + + // the UI below the sash + mStackTraceBase = new Composite(mThreadBase, SWT.NONE); + mStackTraceBase.setLayout(new GridLayout(2, false)); + + mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH); + mRefreshStackTraceButton.setText("Refresh"); + mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + ThreadInfo selectedThread = getThreadSelection(null); + if (selectedThread != null) { + Client currentClient = getCurrentClient(); + if (currentClient != null) { + currentClient.requestThreadStackTrace(selectedThread.getThreadId()); + } + } + } + }); + + mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE); + mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mStackTracePanel = new StackTracePanel(); + mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, + PREFS_STACK_COL_CLASS, + PREFS_STACK_COL_METHOD, + PREFS_STACK_COL_FILE, + PREFS_STACK_COL_LINE, + PREFS_STACK_COL_NATIVE, + store); + + GridData gd; + mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.horizontalSpan = 2; + + // now setup the sash. + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mThreadTable.setLayoutData(data); + + final FormData sashData = new FormData(); + if (store != null && store.contains(PREFS_THREAD_SASH)) { + sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mStackTraceBase.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = mThreadBase.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + store.setValue(PREFS_THREAD_SASH, e.y); + mThreadBase.layout(); + } + } + }); + + ((StackLayout)mBase.getLayout()).topControl = mNotSelected; + + return mBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mThreadTable.setFocus(); + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 || + (changeMask & Client.CHANGE_THREAD_DATA) != 0) { + try { + mThreadTable.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) { + try { + mThreadTable.getDisplay().asyncExec(new Runnable() { + public void run() { + updateThreadStackCall(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mThreadTable.isDisposed()) { + return; + } + + Client client = getCurrentClient(); + + mStackTracePanel.setCurrentClient(client); + + if (client != null) { + if (!client.isThreadUpdateEnabled()) { + ((StackLayout)mBase.getLayout()).topControl = mNotEnabled; + mThreadViewer.setInput(null); + + // if we are currently updating the thread, stop doing it. + mMustStopRecurringThreadUpdate = true; + } else { + ((StackLayout)mBase.getLayout()).topControl = mThreadBase; + mThreadViewer.setInput(client); + + synchronized (mLock) { + // if we're not updating we start the process + if (mRecurringThreadUpdateRunning == false) { + startRecurringThreadUpdate(); + } else if (mMustStopRecurringThreadUpdate) { + // else if there's a runnable that's still going to get called, lets + // simply cancel the stop, and keep going + mMustStopRecurringThreadUpdate = false; + } + } + } + } else { + ((StackLayout)mBase.getLayout()).topControl = mNotSelected; + mThreadViewer.setInput(null); + } + + mBase.layout(); + } + + /** + * Updates the stack call of the currently selected thread. + * <p/> + * This <b>must</b> be called from the UI thread. + */ + private void updateThreadStackCall() { + Client client = getCurrentClient(); + if (client != null) { + // get the current selection in the ThreadTable + ThreadInfo selectedThread = getThreadSelection(null); + + if (selectedThread != null) { + updateThreadStackTrace(selectedThread); + } else { + updateThreadStackTrace(null); + } + } + } + + /** + * updates the stackcall of the specified thread. If <code>null</code> the UI is emptied + * of current data. + * @param thread + */ + private void updateThreadStackTrace(ThreadInfo thread) { + mStackTracePanel.setViewerInput(thread); + + if (thread != null) { + mRefreshStackTraceButton.setEnabled(true); + long stackcallTime = thread.getStackCallTime(); + if (stackcallTime != 0) { + String label = new Date(stackcallTime).toString(); + mStackTraceTimeLabel.setText(label); + } else { + mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ + } + } else { + mRefreshStackTraceButton.setEnabled(true); + mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mThreadTable); + addTableToFocusListener(mStackTraceTable); + } + + /** + * Initiate recurring events. We use a shorter "initialWait" so we do the + * first execution sooner. We don't do it immediately because we want to + * give the clients a chance to get set up. + */ + private void startRecurringThreadUpdate() { + mRecurringThreadUpdateRunning = true; + int initialWait = 1000; + + mDisplay.timerExec(initialWait, new Runnable() { + public void run() { + synchronized (mLock) { + // lets check we still want updates. + if (mMustStopRecurringThreadUpdate == false) { + Client client = getCurrentClient(); + if (client != null) { + client.requestThreadUpdate(); + + mDisplay.timerExec( + DdmUiPreferences.getThreadRefreshInterval() * 1000, this); + } else { + // we don't have a Client, which means the runnable is not + // going to be called through the timer. We reset the running flag. + mRecurringThreadUpdateRunning = false; + } + } else { + // else actually stops (don't call the timerExec) and reset the flags. + mRecurringThreadUpdateRunning = false; + mMustStopRecurringThreadUpdate = false; + } + } + } + }); + } + + /** + * Returns the current thread selection or <code>null</code> if none is found. + * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection + * is returned, otherwise, the <code>ISelection</code> returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or <code>null</code> + */ + private ThreadInfo getThreadSelection(ISelection selection) { + if (selection == null) { + selection = mThreadViewer.getSelection(); + } + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof ThreadInfo) { + return (ThreadInfo)object; + } + } + + return null; + } + +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java new file mode 100644 index 0000000..f65dafe --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/WritePng.java @@ -0,0 +1,258 @@ +/* + * 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 com.android.ddmlib.Log; + +import org.eclipse.swt.graphics.ImageData; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.CRC32; +import java.util.zip.Deflater; + +/** + * Compensate for SWT issues by writing our own PNGs from ImageData. + */ +public class WritePng { + private WritePng() {} + + private static final byte[] PNG_MAGIC = + new byte[] { -119, 80, 78, 71, 13, 10, 26, 10 }; + + public static void savePng(String fileName, ImageData imageData) + throws IOException { + + try { + FileOutputStream out = new FileOutputStream(fileName); + + Log.d("ddms", "Saving to PNG, width=" + imageData.width + + ", height=" + imageData.height + + ", depth=" + imageData.depth + + ", bpl=" + imageData.bytesPerLine); + + savePng(out, imageData); + + // need to do that on, or the file is empty on windows! + out.flush(); + out.close(); + } catch (Exception e) { + Log.e("writepng", e); + } + } + + /* + * Supply functionality missing from our version of SWT. + */ + private static void savePng(OutputStream out, ImageData imageData) + throws IOException { + + int width = imageData.width; + int height = imageData.height; + byte[] out24; + + Log.i("ddms-png", "Convert to 24bit from " + imageData.depth); + + if (imageData.depth == 24 || imageData.depth == 32) { + out24 = convertTo24ForPng(imageData.data, width, height, + imageData.depth, imageData.bytesPerLine); + } else if (imageData.depth == 16) { + out24 = convert16to24(imageData); + } else { + return; + } + + // Create the compressed form. I'm taking the low road here and + // just creating a large buffer, which should always be enough to + // hold the compressed output. + byte[] compPixels = new byte[out24.length + 16384]; + Deflater compressor = new Deflater(); + compressor.setLevel(Deflater.BEST_COMPRESSION); + compressor.setInput(out24); + compressor.finish(); + int compLen; + do { // must do this in a loop to satisfy java.util.Zip + compLen = compressor.deflate(compPixels); + assert compLen != 0 || !compressor.needsInput(); + } while (compLen == 0); + Log.d("ddms", "Compressed image data from " + out24.length + + " to " + compLen); + + // Write the PNG magic + out.write(PNG_MAGIC); + + ByteBuffer buf; + CRC32 crc; + + // Write the IHDR chunk (13 bytes) + byte[] header = new byte[8 + 13 + 4]; + buf = ByteBuffer.wrap(header); + buf.order(ByteOrder.BIG_ENDIAN); + + putChunkHeader(buf, 13, "IHDR"); + buf.putInt(width); + buf.putInt(height); + buf.put((byte) 8); // 8pp + buf.put((byte) 2); // direct color used + buf.put((byte) 0); // compression method == deflate + buf.put((byte) 0); // filter method (none) + buf.put((byte) 0); // interlace method (none) + + crc = new CRC32(); + crc.update(header, 4, 4+13); + buf.putInt((int)crc.getValue()); + + out.write(header); + + // Write the IDAT chunk + byte[] datHdr = new byte[8 + 0 + 4]; + buf = ByteBuffer.wrap(datHdr); + buf.order(ByteOrder.BIG_ENDIAN); + + putChunkHeader(buf, compLen, "IDAT"); + crc = new CRC32(); + crc.update(datHdr, 4, 4+0); + crc.update(compPixels, 0, compLen); + buf.putInt((int) crc.getValue()); + + out.write(datHdr, 0, 8); + out.write(compPixels, 0, compLen); + out.write(datHdr, 8, 4); + + // Write the IEND chunk (0 bytes) + byte[] trailer = new byte[8 + 0 + 4]; + + buf = ByteBuffer.wrap(trailer); + buf.order(ByteOrder.BIG_ENDIAN); + putChunkHeader(buf, 0, "IEND"); + + crc = new CRC32(); + crc.update(trailer, 4, 4+0); + buf.putInt((int)crc.getValue()); + + out.write(trailer); + } + + /* + * Output a chunk header. + */ + private static void putChunkHeader(ByteBuffer buf, int length, + String typeStr) { + + int type = 0; + + if (typeStr.length() != 4) + throw new RuntimeException(); + + for (int i = 0; i < 4; i++) { + type <<= 8; + type |= (byte) typeStr.charAt(i); + } + + buf.putInt(length); + buf.putInt(type); + } + + /* + * Convert raw pixels to 24-bit RGB with a "filter" byte at the start + * of each row. + */ + private static byte[] convertTo24ForPng(byte[] in, int width, int height, + int depth, int stride) { + + assert depth == 24 || depth == 32; + assert stride == width * (depth/8); + + // 24 bit pixels plus one byte per line for "filter" + byte[] out24 = new byte[width * height * 3 + height]; + int y; + + int inOff = 0; + int outOff = 0; + for (y = 0; y < height; y++) { + out24[outOff++] = 0; // filter flag + + if (depth == 24) { + System.arraycopy(in, inOff, out24, outOff, width * 3); + outOff += width * 3; + } else if (depth == 32) { + int tmpOff = inOff; + for (int x = 0; x < width; x++) { + tmpOff++; // ignore alpha + out24[outOff++] = in[tmpOff++]; + out24[outOff++] = in[tmpOff++]; + out24[outOff++] = in[tmpOff++]; + } + } + + inOff += stride; + } + + assert outOff == out24.length; + + return out24; + } + + private static byte[] convert16to24(ImageData imageData) { + int width = imageData.width; + int height = imageData.height; + + int redShift = imageData.palette.redShift; + int greenShift = imageData.palette.greenShift; + int blueShift = imageData.palette.blueShift; + + int redMask = imageData.palette.redMask; + int greenMask = imageData.palette.greenMask; + int blueMask = imageData.palette.blueMask; + + // 24 bit pixels plus one byte per line for "filter" + byte[] out24 = new byte[width * height * 3 + height]; + int outOff = 0; + + + int[] line = new int[width]; + for (int y = 0; y < height; y++) { + imageData.getPixels(0, y, width, line, 0); + + out24[outOff++] = 0; // filter flag + for (int x = 0; x < width; x++) { + int pixelValue = line[x]; + out24[outOff++] = byteChannelValue(pixelValue, redMask, redShift); + out24[outOff++] = byteChannelValue(pixelValue, greenMask, greenShift); + out24[outOff++] = byteChannelValue(pixelValue, blueMask, blueShift); + } + } + + return out24; + } + + private static byte byteChannelValue(int value, int mask, int shift) { + int bValue = value & mask; + if (shift < 0) { + bValue = bValue >>> -shift; + } else { + bValue = bValue << shift; + } + + return (byte)bValue; + + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java new file mode 100644 index 0000000..856b874 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java @@ -0,0 +1,42 @@ +/* + * 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.actions; + +/** + * Common interface for basic action handling. This allows the common ui + * components to access ToolItem or Action the same way. + */ +public interface ICommonAction { + /** + * Sets the enabled state of this action. + * @param enabled <code>true</code> to enable, and + * <code>false</code> to disable + */ + public void setEnabled(boolean enabled); + + /** + * Sets the checked status of this action. + * @param checked the new checked status + */ + public void setChecked(boolean checked); + + /** + * Sets the {@link Runnable} that will be executed when the action is triggered. + */ + public void setRunnable(Runnable runnable); +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java new file mode 100644 index 0000000..bc1598f --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java @@ -0,0 +1,68 @@ +/* + * 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.actions; + +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; + +/** + * Wrapper around {@link ToolItem} to implement {@link ICommonAction} + */ +public class ToolItemAction implements ICommonAction { + public ToolItem item; + + public ToolItemAction(ToolBar parent, int style) { + item = new ToolItem(parent, style); + } + + /** + * Sets the enabled state of this action. + * @param enabled <code>true</code> to enable, and + * <code>false</code> to disable + * @see ICommonAction#setChecked(boolean) + */ + public void setChecked(boolean checked) { + item.setSelection(checked); + } + + /** + * Sets the enabled state of this action. + * @param enabled <code>true</code> to enable, and + * <code>false</code> to disable + * @see ICommonAction#setEnabled(boolean) + */ + public void setEnabled(boolean enabled) { + item.setEnabled(enabled); + } + + /** + * Sets the {@link Runnable} that will be executed when the action is triggered (through + * {@link SelectionListener#widgetSelected(SelectionEvent)} on the wrapped {@link ToolItem}). + * @see ICommonAction#setRunnable(Runnable) + */ + public void setRunnable(final Runnable runnable) { + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + runnable.run(); + } + }); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java new file mode 100644 index 0000000..8e9e11b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2008 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.annotation; + +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Simple utility annotation used only to mark methods that are executed on the UI thread. + * This annotation's sole purpose is to help reading the source code. It has no additional effect. + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.SOURCE) +public @interface UiThread { +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java new file mode 100644 index 0000000..e767eda --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2008 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.annotation; + +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Simple utility annotation used only to mark methods that are not executed on the UI thread. + * This annotation's sole purpose is to help reading the source code. It has no additional effect. + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.SOURCE) +public @interface WorkerThread { +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java new file mode 100644 index 0000000..4df4376 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java @@ -0,0 +1,91 @@ +/* + * 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.console; + + +/** + * Static Console used to ouput messages. By default outputs the message to System.out and + * System.err, but can receive a IDdmConsole object which will actually do something. + */ +public class DdmConsole { + + private static IDdmConsole mConsole; + + /** + * Prints a message to the android console. + * @param message the message to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printErrorToConsole(String message) { + if (mConsole != null) { + mConsole.printErrorToConsole(message); + } else { + System.err.println(message); + } + } + + /** + * Prints several messages to the android console. + * @param messages the messages to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printErrorToConsole(String[] messages) { + if (mConsole != null) { + mConsole.printErrorToConsole(messages); + } else { + for (String message : messages) { + System.err.println(message); + } + } + } + + /** + * Prints a message to the android console. + * @param message the message to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printToConsole(String message) { + if (mConsole != null) { + mConsole.printToConsole(message); + } else { + System.out.println(message); + } + } + + /** + * Prints several messages to the android console. + * @param messages the messages to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printToConsole(String[] messages) { + if (mConsole != null) { + mConsole.printToConsole(messages); + } else { + for (String message : messages) { + System.out.println(message); + } + } + } + + /** + * Sets a IDdmConsole to override the default behavior of the console + * @param console The new IDdmConsole + * **/ + public static void setConsole(IDdmConsole console) { + mConsole = console; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java new file mode 100644 index 0000000..3679d41 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java @@ -0,0 +1,47 @@ +/* + * 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.console; + + +/** + * DDMS console interface. + */ +public interface IDdmConsole { + /** + * Prints a message to the android console. + * @param message the message to print + */ + public void printErrorToConsole(String message); + + /** + * Prints several messages to the android console. + * @param messages the messages to print + */ + public void printErrorToConsole(String[] messages); + + /** + * Prints a message to the android console. + * @param message the message to print + */ + public void printToConsole(String message); + + /** + * Prints several messages to the android console. + * @param messages the messages to print + */ + public void printToConsole(String[] messages); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java new file mode 100644 index 0000000..75c19fe --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java @@ -0,0 +1,167 @@ +/* + * 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.explorer; + +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.FileListingService.IListingReceiver; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; + +/** + * Content provider class for device Explorer. + */ +class DeviceContentProvider implements ITreeContentProvider { + + private TreeViewer mViewer; + private FileListingService mFileListingService; + private FileEntry mRootEntry; + + private IListingReceiver sListingReceiver = new IListingReceiver() { + public void setChildren(final FileEntry entry, FileEntry[] children) { + final Tree t = mViewer.getTree(); + if (t != null && t.isDisposed() == false) { + Display display = t.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (t.isDisposed() == false) { + // refresh the entry. + mViewer.refresh(entry); + + // force it open, since on linux and windows + // when getChildren() returns null, the node is + // not considered expanded. + mViewer.setExpandedState(entry, true); + } + } + }); + } + } + } + + public void refreshEntry(final FileEntry entry) { + final Tree t = mViewer.getTree(); + if (t != null && t.isDisposed() == false) { + Display display = t.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (t.isDisposed() == false) { + // refresh the entry. + mViewer.refresh(entry); + } + } + }); + } + } + } + }; + + /** + * + */ + public DeviceContentProvider() { + } + + public void setListingService(FileListingService fls) { + mFileListingService = fls; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object) + */ + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof FileEntry) { + FileEntry parentEntry = (FileEntry)parentElement; + + Object[] oldEntries = parentEntry.getCachedChildren(); + Object[] newEntries = mFileListingService.getChildren(parentEntry, + true, sListingReceiver); + + if (newEntries != null) { + return newEntries; + } else { + // if null was returned, this means the cache was not valid, + // and a thread was launched for ls. sListingReceiver will be + // notified with the new entries. + return oldEntries; + } + } + return new Object[0]; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object) + */ + public Object getParent(Object element) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + return entry.getParent(); + } + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object) + */ + public boolean hasChildren(Object element) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + return entry.getType() == FileListingService.TYPE_DIRECTORY; + } + return false; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object) + */ + public Object[] getElements(Object inputElement) { + if (inputElement instanceof FileEntry) { + FileEntry entry = (FileEntry)inputElement; + if (entry.isRoot()) { + return getChildren(mRootEntry); + } + } + + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IContentProvider#dispose() + */ + public void dispose() { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object) + */ + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + if (viewer instanceof TreeViewer) { + mViewer = (TreeViewer)viewer; + } + if (newInput instanceof FileEntry) { + mRootEntry = (FileEntry)newInput; + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java new file mode 100644 index 0000000..ba0f555 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java @@ -0,0 +1,833 @@ +/* + * 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.explorer; + +import com.android.ddmlib.Device; +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.android.ddmlib.SyncService.SyncResult; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.Panel; +import com.android.ddmuilib.TableHelper; +import com.android.ddmuilib.actions.ICommonAction; +import com.android.ddmuilib.console.DdmConsole; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.ViewerDropAdapter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.FileTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.dnd.TransferData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Device filesystem explorer class. + */ +public class DeviceExplorer extends Panel { + + private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S + private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S + + private static Pattern mKeyFilePattern = Pattern.compile( + "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S + private static Pattern mDataFilePattern = Pattern.compile( + "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S + + public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S + public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S + public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S + public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S + public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S + public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S + + private Composite mParent; + private TreeViewer mTreeViewer; + private Tree mTree; + private DeviceContentProvider mContentProvider; + + private ICommonAction mPushAction; + private ICommonAction mPullAction; + private ICommonAction mDeleteAction; + + private Image mFileImage; + private Image mFolderImage; + private Image mPackageImage; + private Image mOtherImage; + + private Device mCurrentDevice; + + private String mDefaultSave; + + /** + * Implementation of the SyncService.ISyncProgressMonitor. It wraps a jFace IProgressMonitor + * and just forward the calls to the jFace object. + */ + private static class SyncProgressMonitor implements ISyncProgressMonitor { + + private IProgressMonitor mMonitor; + private String mName; + + SyncProgressMonitor(IProgressMonitor monitor, String name) { + mMonitor = monitor; + mName = name; + } + + public void start(int totalWork) { + mMonitor.beginTask(mName, totalWork); + } + + public void stop() { + mMonitor.done(); + } + + public void advance(int work) { + mMonitor.worked(work); + } + + public boolean isCanceled() { + return mMonitor.isCanceled(); + } + + public void startSubTask(String name) { + mMonitor.subTask(name); + } + } + + public DeviceExplorer() { + + } + + /** + * Sets the images for the listview + * @param fileImage + * @param folderImage + * @param otherImage + */ + public void setImages(Image fileImage, Image folderImage, Image packageImage, + Image otherImage) { + mFileImage = fileImage; + mFolderImage = folderImage; + mPackageImage = packageImage; + mOtherImage = otherImage; + } + + /** + * Sets the actions so that the device explorer can enable/disable them based on the current + * selection + * @param pushAction + * @param pullAction + * @param deleteAction + */ + public void setActions(ICommonAction pushAction, ICommonAction pullAction, + ICommonAction deleteAction) { + mPushAction = pushAction; + mPullAction = pullAction; + mDeleteAction = deleteAction; + } + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + parent.setLayout(new FillLayout()); + + mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL); + mTree.setHeaderVisible(true); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + // create columns + TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, + "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT, + "000000", COLUMN_SIZE, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT, + "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT, + "20:54", COLUMN_TIME, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT, + "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT, + "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$ + + // create the jface wrapper + mTreeViewer = new TreeViewer(mTree); + + // setup data provider + mContentProvider = new DeviceContentProvider(); + mTreeViewer.setContentProvider(mContentProvider); + mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage, + mFolderImage, mPackageImage, mOtherImage)); + + // setup a listener for selection + mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection sel = event.getSelection(); + if (sel.isEmpty()) { + mPullAction.setEnabled(false); + mPushAction.setEnabled(false); + mDeleteAction.setEnabled(false); + return; + } + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + Object element = selection.getFirstElement(); + if (element == null) + return; + if (element instanceof FileEntry) { + mPullAction.setEnabled(true); + mPushAction.setEnabled(selection.size() == 1); + if (selection.size() == 1) { + setDeleteEnabledState((FileEntry)element); + } else { + mDeleteAction.setEnabled(false); + } + } + } + } + }); + + // add support for double click + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + ISelection sel = event.getSelection(); + + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + + if (selection.size() == 1) { + FileEntry entry = (FileEntry)selection.getFirstElement(); + String name = entry.getName(); + + FileEntry parentEntry = entry.getParent(); + + // can't really do anything with no parent + if (parentEntry == null) { + return; + } + + // check this is a file like we want. + Matcher m = mKeyFilePattern.matcher(name); + if (m.matches()) { + // get the name w/o the extension + String baseName = m.group(1); + + // add the data extension + String dataName = baseName + TRACE_DATA_EXT; + + FileEntry dataEntry = parentEntry.findChild(dataName); + + handleTraceDoubleClick(baseName, entry, dataEntry); + + } else { + m = mDataFilePattern.matcher(name); + if (m.matches()) { + // get the name w/o the extension + String baseName = m.group(1); + + // add the key extension + String keyName = baseName + TRACE_KEY_EXT; + + FileEntry keyEntry = parentEntry.findChild(keyName); + + handleTraceDoubleClick(baseName, keyEntry, entry); + } + } + } + } + } + }); + + // setup drop listener + mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, + new Transfer[] { FileTransfer.getInstance() }, + new ViewerDropAdapter(mTreeViewer) { + @Override + public boolean performDrop(Object data) { + // get the item on which we dropped the item(s) + FileEntry target = (FileEntry)getCurrentTarget(); + + // in case we drop at the same level as root + if (target == null) { + return false; + } + + // if the target is not a directory, we get the parent directory + if (target.isDirectory() == false) { + target = target.getParent(); + } + + if (target == null) { + return false; + } + + // get the list of files to drop + String[] files = (String[])data; + + // do the drop + pushFiles(files, target); + + // we need to finish with a refresh + refresh(target); + + return true; + } + + @Override + public boolean validateDrop(Object target, int operation, TransferData transferType) { + if (target == null) { + return false; + } + + // convert to the real item + FileEntry targetEntry = (FileEntry)target; + + // if the target is not a directory, we get the parent directory + if (targetEntry.isDirectory() == false) { + target = targetEntry.getParent(); + } + + if (target == null) { + return false; + } + + return true; + } + }); + + // create and start the refresh thread + new Thread("Device Ls refresher") { + @Override + public void run() { + while (true) { + try { + sleep(FileListingService.REFRESH_RATE); + } catch (InterruptedException e) { + return; + } + + if (mTree != null && mTree.isDisposed() == false) { + Display display = mTree.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + mTreeViewer.refresh(true); + } + } + }); + } else { + return; + } + } else { + return; + } + } + + } + }.start(); + + return mTree; + } + + @Override + protected void postCreation() { + + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTree.setFocus(); + } + + /** + * Processes a double click on a trace file + * @param baseName the base name of the 2 files. + * @param keyEntry The FileEntry for the .key file. + * @param dataEntry The FileEntry for the .data file. + */ + private void handleTraceDoubleClick(String baseName, FileEntry keyEntry, + FileEntry dataEntry) { + // first we need to download the files. + File keyFile; + File dataFile; + String path; + try { + // create a temp file for keyFile + File f = File.createTempFile(baseName, ".trace"); + f.delete(); + f.mkdir(); + + path = f.getAbsolutePath(); + + keyFile = new File(path + File.separator + keyEntry.getName()); + dataFile = new File(path + File.separator + dataEntry.getName()); + } catch (IOException e) { + return; + } + + // download the files + SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor(); + SyncResult result = sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), result.getMessage())); + return; + } + + result = sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", dataEntry.getName(), result.getMessage())); + return; + } + + // now that we have the file, we need to launch traceview + String[] command = new String[2]; + command[0] = DdmUiPreferences.getTraceview(); + command[1] = path + File.separator + baseName; + + try { + final Process p = Runtime.getRuntime().exec(command); + + // create a thread for the output + new Thread("Traceview output") { + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(p.getErrorStream()); + BufferedReader resultReader = new BufferedReader(is); + + // read the lines as they come. if null is returned, it's + // because the process finished + try { + while (true) { + String line = resultReader.readLine(); + if (line != null) { + DdmConsole.printErrorToConsole("Traceview: " + line); + } else { + break; + } + } + // get the return code from the process + p.waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + + } + } + }.start(); + + } catch (IOException e) { + } + } + } + + /** + * Pull the current selection on the local drive. This method displays + * a dialog box to let the user select where to store the file(s) and + * folder(s). + */ + public void pullSelection() { + // get the selection + TreeItem[] items = mTree.getSelection(); + + // name of the single file pull, or null if we're pulling a directory + // or more than one object. + String filePullName = null; + FileEntry singleEntry = null; + + // are we pulling a single file? + if (items.length == 1) { + singleEntry = (FileEntry)items[0].getData(); + if (singleEntry.getType() == FileListingService.TYPE_FILE) { + filePullName = singleEntry.getName(); + } + } + + // where do we save by default? + String defaultPath = mDefaultSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + + if (filePullName != null) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); + + fileDialog.setText("Get Device File"); + fileDialog.setFileName(filePullName); + fileDialog.setFilterPath(defaultPath); + + String fileName = fileDialog.open(); + if (fileName != null) { + mDefaultSave = fileDialog.getFilterPath(); + + pullFile(singleEntry, fileName); + } + } else { + DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE); + + directoryDialog.setText("Get Device Files/Folders"); + directoryDialog.setFilterPath(defaultPath); + + String directoryName = directoryDialog.open(); + if (directoryName != null) { + pullSelection(items, directoryName); + } + } + } + + /** + * Push new file(s) and folder(s) into the current selection. Current + * selection must be single item. If the current selection is not a + * directory, the parent directory is used. + * This method displays a dialog to let the user choose file to push to + * the device. + */ + public void pushIntoSelection() { + // get the name of the object we're going to pull + TreeItem[] items = mTree.getSelection(); + + if (items.length == 0) { + return; + } + + FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN); + String fileName; + + dlg.setText("Put File on Device"); + + // There should be only one. + FileEntry entry = (FileEntry)items[0].getData(); + dlg.setFileName(entry.getName()); + + String defaultPath = mDefaultSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + dlg.setFilterPath(defaultPath); + + fileName = dlg.open(); + if (fileName != null) { + mDefaultSave = dlg.getFilterPath(); + + // we need to figure out the remote path based on the current selection type. + String remotePath; + FileEntry toRefresh = entry; + if (entry.isDirectory()) { + remotePath = entry.getFullPath(); + } else { + toRefresh = entry.getParent(); + remotePath = toRefresh.getFullPath(); + } + + pushFile(fileName, remotePath); + mTreeViewer.refresh(toRefresh); + } + } + + public void deleteSelection() { + // get the name of the object we're going to pull + TreeItem[] items = mTree.getSelection(); + + if (items.length != 1) { + return; + } + + FileEntry entry = (FileEntry)items[0].getData(); + final FileEntry parentEntry = entry.getParent(); + + // create the delete command + String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$ + + try { + mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() { + public void addOutput(byte[] data, int offset, int length) { + // pass + // TODO get output to display errors if any. + } + + public void flush() { + mTreeViewer.refresh(parentEntry); + } + + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } + + } + + /** + * Force a full refresh of the explorer. + */ + public void refresh() { + mTreeViewer.refresh(true); + } + + /** + * Sets the new device to explorer + */ + public void switchDevice(final Device device) { + if (device != mCurrentDevice) { + mCurrentDevice = device; + // now we change the input. but we need to do that in the + // ui thread. + if (mTree.isDisposed() == false) { + Display d = mTree.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // new service + if (mCurrentDevice != null) { + FileListingService fls = mCurrentDevice.getFileListingService(); + mContentProvider.setListingService(fls); + mTreeViewer.setInput(fls.getRoot()); + } + } + } + }); + } + } + } + + /** + * Refresh an entry from a non ui thread. + * @param entry the entry to refresh. + */ + private void refresh(final FileEntry entry) { + Display d = mTreeViewer.getTree().getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + mTreeViewer.refresh(entry); + } + }); + } + + /** + * Pulls the selection from a device. + * @param items the tree selection the remote file on the device + * @param localDirector the local directory in which to save the files. + */ + private void pullSelection(TreeItem[] items, final String localDirectory) { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + // make a list of the FileEntry. + ArrayList<FileEntry> entries = new ArrayList<FileEntry>(); + for (TreeItem item : items) { + Object data = item.getData(); + if (data instanceof FileEntry) { + entries.add((FileEntry)data); + } + } + final FileEntry[] entryArray = entries.toArray( + new FileEntry[entries.size()]); + + // get a progressdialog + try { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + // create a monitor wrapper around the jface monitor + SyncResult result = sync.pull(entryArray, localDirectory, + new SyncProgressMonitor(monitor, + "Pulling file(s) from the device")); + + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull selection: %1$s", result.getMessage())); + } + sync.close(); + } + }); + } catch (InvocationTargetException e) { + DdmConsole.printErrorToConsole( "Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } catch (InterruptedException e) { + DdmConsole.printErrorToConsole("Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + } + + /** + * Pulls a file from a device. + * @param remote the remote file on the device + * @param local the destination filepath + */ + private void pullFile(final FileEntry remote, final String local) { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + try { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + SyncResult result = sync.pullFile(remote, local, new SyncProgressMonitor( + monitor, String.format("Pulling %1$s from the device", + remote.getName()))); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", remote, result.getMessage())); + } + + sync.close(); + } + }); + } catch (InvocationTargetException e) { + DdmConsole.printErrorToConsole( "Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } catch (InterruptedException e) { + DdmConsole.printErrorToConsole("Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + } + + /** + * Pushes several files and directory into a remote directory. + * @param localFiles + * @param remoteDirectory + */ + private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + try { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + SyncResult result = sync.push(localFiles, remoteDirectory, + new SyncProgressMonitor(monitor, + "Pushing file(s) to the device")); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push the items: %1$s", result.getMessage())); + } + + sync.close(); + } + }); + } catch (InvocationTargetException e) { + DdmConsole.printErrorToConsole("Failed to push the items"); + DdmConsole.printErrorToConsole(e.getMessage()); + } catch (InterruptedException e) { + DdmConsole.printErrorToConsole("Failed to push the items"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + return; + } + } + + /** + * Pushes a file on a device. + * @param local the local filepath of the file to push + * @param remoteDirectory the remote destination directory on the device + */ + private void pushFile(final String local, final String remoteDirectory) { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + try { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + // get the file name + String[] segs = local.split(Pattern.quote(File.separator)); + String name = segs[segs.length-1]; + String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR + + name; + + SyncResult result = sync.pushFile(local, remoteFile, + new SyncProgressMonitor(monitor, + String.format("Pushing %1$s to the device.", name))); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push %1$s on %2$s: %3$s", + name, mCurrentDevice.getSerialNumber(), result.getMessage())); + } + + sync.close(); + } + }); + } catch (InvocationTargetException e) { + DdmConsole.printErrorToConsole("Failed to push the item(s)."); + DdmConsole.printErrorToConsole(e.getMessage()); + } catch (InterruptedException e) { + DdmConsole.printErrorToConsole("Failed to push the item(s)."); + DdmConsole.printErrorToConsole(e.getMessage()); + } + return; + } + } + + /** + * Sets the enabled state based on a FileEntry properties + * @param element The selected FileEntry + */ + protected void setDeleteEnabledState(FileEntry element) { + mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java new file mode 100644 index 0000000..1dca962 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java @@ -0,0 +1,152 @@ +/* + * 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.explorer; + +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.FileListingService.FileEntry; + +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; + +/** + * Label provider for the FileEntry. + */ +class FileLabelProvider implements ILabelProvider, ITableLabelProvider { + + private Image mFileImage; + private Image mFolderImage; + private Image mPackageImage; + private Image mOtherImage; + + /** + * Creates Label provider with custom images. + * @param fileImage the Image to represent a file + * @param folderImage the Image to represent a folder + * @param packageImage the Image to represent a .apk file. If null, + * fileImage is used instead. + * @param otherImage the Image to represent all other entry type. + */ + public FileLabelProvider(Image fileImage, Image folderImage, + Image packageImage, Image otherImage) { + mFileImage = fileImage; + mFolderImage = folderImage; + mOtherImage = otherImage; + if (packageImage != null) { + mPackageImage = packageImage; + } else { + mPackageImage = fileImage; + } + } + + /** + * Creates a label provider with default images. + * + */ + public FileLabelProvider() { + + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object) + */ + public Image getImage(Object element) { + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object) + */ + public String getText(Object element) { + return null; + } + + public Image getColumnImage(Object element, int columnIndex) { + if (columnIndex == 0) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + switch (entry.getType()) { + case FileListingService.TYPE_FILE: + case FileListingService.TYPE_LINK: + // get the name and extension + if (entry.isApplicationPackage()) { + return mPackageImage; + } + return mFileImage; + case FileListingService.TYPE_DIRECTORY: + case FileListingService.TYPE_DIRECTORY_LINK: + return mFolderImage; + } + } + + // default case return a different image. + return mOtherImage; + } + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + switch (columnIndex) { + case 0: + return entry.getName(); + case 1: + return entry.getSize(); + case 2: + return entry.getDate(); + case 3: + return entry.getTime(); + case 4: + return entry.getPermissions(); + case 5: + return entry.getInfo(); + } + } + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener) + */ + public void addListener(ILabelProviderListener listener) { + // we don't need listeners. + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose() + */ + public void dispose() { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String) + */ + public boolean isLabelProperty(Object element, String property) { + return false; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener) + */ + public void removeListener(ILabelProviderListener listener) { + // we don't need listeners + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java new file mode 100644 index 0000000..578a7ac --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2008 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.location; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; + +/** + * Encapsulation of controls handling a location coordinate in decimal and sexagesimal. + * <p/>This handle the conversion between both modes automatically by using a {@link ModifyListener} + * on all the {@link Text} widgets. + * <p/>To get/set the coordinate, use {@link #setValue(double)} and {@link #getValue()} (preceded by + * a call to {@link #isValueValid()}) + */ +public final class CoordinateControls { + private double mValue; + private boolean mValueValidity = false; + private Text mDecimalText; + private Text mSexagesimalDegreeText; + private Text mSexagesimalMinuteText; + private Text mSexagesimalSecondText; + + /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)} + * is called. This is an int instead of a boolean to act as a counter. */ + private int mManualTextChange = 0; + + /** + * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode. + */ + private ModifyListener mSexagesimalListener = new ModifyListener() { + public void modifyText(ModifyEvent event) { + if (mManualTextChange > 0) { + return; + } + try { + mValue = getValueFromSexagesimalControls(); + setValueIntoDecimalControl(mValue); + mValueValidity = true; + } catch (NumberFormatException e) { + // wrong format empty the decimal controls. + mValueValidity = false; + resetDecimalControls(); + } + } + }; + + /** + * Creates the {@link Text} control for the decimal display of the coordinate. + * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createDecimalText(Composite parent) { + mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() { + public void modifyText(ModifyEvent event) { + if (mManualTextChange > 0) { + return; + } + try { + mValue = Double.parseDouble(mDecimalText.getText()); + setValueIntoSexagesimalControl(mValue); + mValueValidity = true; + } catch (NumberFormatException e) { + // wrong format empty the sexagesimal controls. + mValueValidity = false; + resetSexagesimalControls(); + } + } + }); + } + + /** + * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal + * mode. + * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalDegreeText(Composite parent) { + mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal + * mode. + * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalMinuteText(Composite parent) { + mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal + * mode. + * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalSecondText(Composite parent) { + mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Sets the coordinate into the {@link Text} controls. + * @param value the coordinate value to set. + */ + public void setValue(double value) { + mValue = value; + mValueValidity = true; + setValueIntoDecimalControl(value); + setValueIntoSexagesimalControl(value); + } + + /** + * Returns whether the value in the control(s) is valid. + */ + public boolean isValueValid() { + return mValueValidity; + } + + /** + * Returns the current value set in the control(s). + * <p/>This value can be erroneous, and a check with {@link #isValueValid()} should be performed + * before any call to this method. + */ + public double getValue() { + return mValue; + } + + /** + * Enables or disables all the {@link Text} controls. + * @param enabled the enabled state. + */ + public void setEnabled(boolean enabled) { + mDecimalText.setEnabled(enabled); + mSexagesimalDegreeText.setEnabled(enabled); + mSexagesimalMinuteText.setEnabled(enabled); + mSexagesimalSecondText.setEnabled(enabled); + } + + private void resetDecimalControls() { + mManualTextChange++; + mDecimalText.setText(""); //$NON-NLS-1$ + mManualTextChange--; + } + + private void resetSexagesimalControls() { + mManualTextChange++; + mSexagesimalDegreeText.setText(""); //$NON-NLS-1$ + mSexagesimalMinuteText.setText(""); //$NON-NLS-1$ + mSexagesimalSecondText.setText(""); //$NON-NLS-1$ + mManualTextChange--; + } + + /** + * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener} + * @param parent the parent {@link Composite}. + * @param defaultString the default string to be used to compute the {@link Text} control + * size hint. + * @param listener the {@link ModifyListener} to be called when the {@link Text} control is + * modified. + */ + private Text createTextControl(Composite parent, String defaultString, + ModifyListener listener) { + // create the control + Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + + // add the standard listener to it. + text.addModifyListener(listener); + + // compute its size/ + mManualTextChange++; + text.setText(defaultString); + text.pack(); + Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT); + text.setText(""); //$NON-NLS-1$ + mManualTextChange--; + + GridData gridData = new GridData(); + gridData.widthHint = size.x; + text.setLayoutData(gridData); + + return text; + } + + private double getValueFromSexagesimalControls() throws NumberFormatException { + double degrees = Double.parseDouble(mSexagesimalDegreeText.getText()); + double minutes = Double.parseDouble(mSexagesimalMinuteText.getText()); + double seconds = Double.parseDouble(mSexagesimalSecondText.getText()); + + boolean isPositive = (degrees >= 0.); + degrees = Math.abs(degrees); + + double value = degrees + minutes / 60. + seconds / 3600.; + return isPositive ? value : - value; + } + + private void setValueIntoDecimalControl(double value) { + mManualTextChange++; + mDecimalText.setText(String.format("%.6f", value)); + mManualTextChange--; + } + + private void setValueIntoSexagesimalControl(double value) { + // get the sign and make the number positive no matter what. + boolean isPositive = (value >= 0.); + value = Math.abs(value); + + // get the degree + double degrees = Math.floor(value); + + // get the minutes + double minutes = Math.floor((value - degrees) * 60.); + + // get the seconds. + double seconds = (value - degrees) * 3600. - minutes * 60.; + + mManualTextChange++; + mSexagesimalDegreeText.setText( + Integer.toString(isPositive ? (int)degrees : (int)- degrees)); + mSexagesimalMinuteText.setText(Integer.toString((int)minutes)); + mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$ + mManualTextChange--; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java new file mode 100644 index 0000000..a30337a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2008 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.location; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * A very basic GPX parser to meet the need of the emulator control panel. + * <p/> + * It parses basic waypoint information, and tracks (merging segments). + */ +public class GpxParser { + + private final static String NS_GPX = "http://www.topografix.com/GPX/1/1"; //$NON-NLS-1$ + + private final static String NODE_WAYPOINT = "wpt"; //$NON-NLS-1$ + private final static String NODE_TRACK = "trk"; //$NON-NLS-1$ + private final static String NODE_TRACK_SEGMENT = "trkseg"; //$NON-NLS-1$ + private final static String NODE_TRACK_POINT = "trkpt"; //$NON-NLS-1$ + private final static String NODE_NAME = "name"; //$NON-NLS-1$ + private final static String NODE_TIME = "time"; //$NON-NLS-1$ + private final static String NODE_ELEVATION = "ele"; //$NON-NLS-1$ + private final static String NODE_DESCRIPTION = "desc"; //$NON-NLS-1$ + private final static String ATTR_LONGITUDE = "lon"; //$NON-NLS-1$ + private final static String ATTR_LATITUDE = "lat"; //$NON-NLS-1$ + + private static SAXParserFactory sParserFactory; + + static { + sParserFactory = SAXParserFactory.newInstance(); + sParserFactory.setNamespaceAware(true); + } + + private String mFileName; + + private GpxHandler mHandler; + + /** Pattern to parse time with optional sub-second precision, and optional + * Z indicating the time is in UTC. */ + private final static Pattern ISO8601_TIME = + Pattern.compile("(\\d{4})-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:(\\.\\d+))?(Z)?"); //$NON-NLS-1$ + + /** + * Handler for the SAX parser. + */ + private static class GpxHandler extends DefaultHandler { + // --------- parsed data --------- + List<WayPoint> mWayPoints; + List<Track> mTrackList; + + // --------- state for parsing --------- + Track mCurrentTrack; + TrackPoint mCurrentTrackPoint; + WayPoint mCurrentWayPoint; + final StringBuilder mStringAccumulator = new StringBuilder(); + + boolean mSuccess = true; + + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + // we only care about the standard GPX nodes. + try { + if (NS_GPX.equals(uri)) { + if (NODE_WAYPOINT.equals(localName)) { + if (mWayPoints == null) { + mWayPoints = new ArrayList<WayPoint>(); + } + + mWayPoints.add(mCurrentWayPoint = new WayPoint()); + handleLocation(mCurrentWayPoint, attributes); + } else if (NODE_TRACK.equals(localName)) { + if (mTrackList == null) { + mTrackList = new ArrayList<Track>(); + } + + mTrackList.add(mCurrentTrack = new Track()); + } else if (NODE_TRACK_SEGMENT.equals(localName)) { + // for now we do nothing here. This will merge all the segments into + // a single TrackPoint list in the Track. + } else if (NODE_TRACK_POINT.equals(localName)) { + if (mCurrentTrack != null) { + mCurrentTrack.addPoint(mCurrentTrackPoint = new TrackPoint()); + handleLocation(mCurrentTrackPoint, attributes); + } + } + } + } finally { + // no matter the node, we empty the StringBuilder accumulator when we start + // a new node. + mStringAccumulator.setLength(0); + } + } + + /** + * Processes new characters for the node content. The characters are simply stored, + * and will be processed when {@link #endElement(String, String, String)} is called. + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + mStringAccumulator.append(ch, start, length); + } + + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (NS_GPX.equals(uri)) { + if (NODE_WAYPOINT.equals(localName)) { + mCurrentWayPoint = null; + } else if (NODE_TRACK.equals(localName)) { + mCurrentTrack = null; + } else if (NODE_TRACK_POINT.equals(localName)) { + mCurrentTrackPoint = null; + } else if (NODE_NAME.equals(localName)) { + if (mCurrentTrack != null) { + mCurrentTrack.setName(mStringAccumulator.toString()); + } else if (mCurrentWayPoint != null) { + mCurrentWayPoint.setName(mStringAccumulator.toString()); + } + } else if (NODE_TIME.equals(localName)) { + if (mCurrentTrackPoint != null) { + mCurrentTrackPoint.setTime(computeTime(mStringAccumulator.toString())); + } + } else if (NODE_ELEVATION.equals(localName)) { + if (mCurrentTrackPoint != null) { + mCurrentTrackPoint.setElevation( + Double.parseDouble(mStringAccumulator.toString())); + } else if (mCurrentWayPoint != null) { + mCurrentWayPoint.setElevation( + Double.parseDouble(mStringAccumulator.toString())); + } + } else if (NODE_DESCRIPTION.equals(localName)) { + if (mCurrentWayPoint != null) { + mCurrentWayPoint.setDescription(mStringAccumulator.toString()); + } + } + } + } + + @Override + public void error(SAXParseException e) throws SAXException { + mSuccess = false; + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + mSuccess = false; + } + + /** + * Converts the string description of the time into milliseconds since epoch. + * @param timeString the string data. + * @return date in milliseconds. + */ + private long computeTime(String timeString) { + // Time looks like: 2008-04-05T19:24:50Z + Matcher m = ISO8601_TIME.matcher(timeString); + if (m.matches()) { + // get the various elements and reconstruct time as a long. + try { + int year = Integer.parseInt(m.group(1)); + int month = Integer.parseInt(m.group(2)); + int date = Integer.parseInt(m.group(3)); + int hourOfDay = Integer.parseInt(m.group(4)); + int minute = Integer.parseInt(m.group(5)); + int second = Integer.parseInt(m.group(6)); + + // handle the optional parameters. + int milliseconds = 0; + + String subSecondGroup = m.group(7); + if (subSecondGroup != null) { + milliseconds = (int)(1000 * Double.parseDouble(subSecondGroup)); + } + + boolean utcTime = m.group(8) != null; + + // now we convert into milliseconds since epoch. + Calendar c; + if (utcTime) { + c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$ + } else { + c = Calendar.getInstance(); + } + + c.set(year, month, date, hourOfDay, minute, second); + + return c.getTimeInMillis() + milliseconds; + } catch (NumberFormatException e) { + // format is invalid, we'll return -1 below. + } + + } + + // invalid time! + return -1; + } + + /** + * Handles the location attributes and store them into a {@link LocationPoint}. + * @param locationNode the {@link LocationPoint} to receive the location data. + * @param attributes the attributes from the XML node. + */ + private void handleLocation(LocationPoint locationNode, Attributes attributes) { + try { + double longitude = Double.parseDouble(attributes.getValue(ATTR_LONGITUDE)); + double latitude = Double.parseDouble(attributes.getValue(ATTR_LATITUDE)); + + locationNode.setLocation(longitude, latitude); + } catch (NumberFormatException e) { + // wrong data, do nothing. + } + } + + WayPoint[] getWayPoints() { + if (mWayPoints != null) { + return mWayPoints.toArray(new WayPoint[mWayPoints.size()]); + } + + return null; + } + + Track[] getTracks() { + if (mTrackList != null) { + return mTrackList.toArray(new Track[mTrackList.size()]); + } + + return null; + } + + boolean getSuccess() { + return mSuccess; + } + } + + /** + * A GPS track. + * <p/>A track is composed of a list of {@link TrackPoint} and optional name and comment. + */ + public final static class Track { + private String mName; + private String mComment; + private List<TrackPoint> mPoints = new ArrayList<TrackPoint>(); + + void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + void setComment(String comment) { + mComment = comment; + } + + public String getComment() { + return mComment; + } + + void addPoint(TrackPoint trackPoint) { + mPoints.add(trackPoint); + } + + public TrackPoint[] getPoints() { + return mPoints.toArray(new TrackPoint[mPoints.size()]); + } + + public long getFirstPointTime() { + if (mPoints.size() > 0) { + return mPoints.get(0).getTime(); + } + + return -1; + } + + public long getLastPointTime() { + if (mPoints.size() > 0) { + return mPoints.get(mPoints.size()-1).getTime(); + } + + return -1; + } + + public int getPointCount() { + return mPoints.size(); + } + } + + /** + * Creates a new GPX parser for a file specified by its full path. + * @param fileName The full path of the GPX file to parse. + */ + public GpxParser(String fileName) { + mFileName = fileName; + } + + /** + * Parses the GPX file. + * @return <code>true</code> if success. + */ + public boolean parse() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + mHandler = new GpxHandler(); + + parser.parse(new InputSource(new FileReader(mFileName)), mHandler); + + return mHandler.getSuccess(); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return false; + } + + /** + * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or + * if the parsing failed. + */ + public WayPoint[] getWayPoints() { + if (mHandler != null) { + return mHandler.getWayPoints(); + } + + return null; + } + + /** + * Returns the parsed {@link Track} objects, or <code>null</code> if none were found (or + * if the parsing failed. + */ + public Track[] getTracks() { + if (mHandler != null) { + return mHandler.getTracks(); + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java new file mode 100644 index 0000000..af485ac --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2008 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.location; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * A very basic KML parser to meet the need of the emulator control panel. + * <p/> + * It parses basic Placemark information. + */ +public class KmlParser { + + private final static String NS_KML_2 = "http://earth.google.com/kml/2."; //$NON-NLS-1$ + + private final static String NODE_PLACEMARK = "Placemark"; //$NON-NLS-1$ + private final static String NODE_NAME = "name"; //$NON-NLS-1$ + private final static String NODE_COORDINATES = "coordinates"; //$NON-NLS-1$ + + private final static Pattern sLocationPattern = Pattern.compile("([^,]+),([^,]+)(?:,([^,]+))?"); + + private static SAXParserFactory sParserFactory; + + static { + sParserFactory = SAXParserFactory.newInstance(); + sParserFactory.setNamespaceAware(true); + } + + private String mFileName; + + private KmlHandler mHandler; + + /** + * Handler for the SAX parser. + */ + private static class KmlHandler extends DefaultHandler { + // --------- parsed data --------- + List<WayPoint> mWayPoints; + + // --------- state for parsing --------- + WayPoint mCurrentWayPoint; + final StringBuilder mStringAccumulator = new StringBuilder(); + + boolean mSuccess = true; + + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + // we only care about the standard GPX nodes. + try { + if (uri.startsWith(NS_KML_2)) { + if (NODE_PLACEMARK.equals(localName)) { + if (mWayPoints == null) { + mWayPoints = new ArrayList<WayPoint>(); + } + + mWayPoints.add(mCurrentWayPoint = new WayPoint()); + } + } + } finally { + // no matter the node, we empty the StringBuilder accumulator when we start + // a new node. + mStringAccumulator.setLength(0); + } + } + + /** + * Processes new characters for the node content. The characters are simply stored, + * and will be processed when {@link #endElement(String, String, String)} is called. + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + mStringAccumulator.append(ch, start, length); + } + + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (uri.startsWith(NS_KML_2)) { + if (NODE_PLACEMARK.equals(localName)) { + mCurrentWayPoint = null; + } else if (NODE_NAME.equals(localName)) { + if (mCurrentWayPoint != null) { + mCurrentWayPoint.setName(mStringAccumulator.toString()); + } + } else if (NODE_COORDINATES.equals(localName)) { + if (mCurrentWayPoint != null) { + parseLocation(mCurrentWayPoint, mStringAccumulator.toString()); + } + } + } + } + + @Override + public void error(SAXParseException e) throws SAXException { + mSuccess = false; + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + mSuccess = false; + } + + /** + * Parses the location string and store the information into a {@link LocationPoint}. + * @param locationNode the {@link LocationPoint} to receive the location data. + * @param location The string containing the location info. + */ + private void parseLocation(LocationPoint locationNode, String location) { + Matcher m = sLocationPattern.matcher(location); + if (m.matches()) { + try { + double longitude = Double.parseDouble(m.group(1)); + double latitude = Double.parseDouble(m.group(2)); + + locationNode.setLocation(longitude, latitude); + + if (m.groupCount() == 3) { + // looks like we have elevation data. + locationNode.setElevation(Double.parseDouble(m.group(3))); + } + } catch (NumberFormatException e) { + // wrong data, do nothing. + } + } + } + + WayPoint[] getWayPoints() { + if (mWayPoints != null) { + return mWayPoints.toArray(new WayPoint[mWayPoints.size()]); + } + + return null; + } + + boolean getSuccess() { + return mSuccess; + } + } + + /** + * Creates a new GPX parser for a file specified by its full path. + * @param fileName The full path of the GPX file to parse. + */ + public KmlParser(String fileName) { + mFileName = fileName; + } + + /** + * Parses the GPX file. + * @return <code>true</code> if success. + */ + public boolean parse() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + mHandler = new KmlHandler(); + + parser.parse(new InputSource(new FileReader(mFileName)), mHandler); + + return mHandler.getSuccess(); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return false; + } + + /** + * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or + * if the parsing failed. + */ + public WayPoint[] getWayPoints() { + if (mHandler != null) { + return mHandler.getWayPoints(); + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java new file mode 100644 index 0000000..dbb8f41 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2008 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.location; + +/** + * Base class for Location aware points. + */ +class LocationPoint { + private double mLongitude; + private double mLatitude; + private boolean mHasElevation = false; + private double mElevation; + + final void setLocation(double longitude, double latitude) { + mLongitude = longitude; + mLatitude = latitude; + } + + public final double getLongitude() { + return mLongitude; + } + + public final double getLatitude() { + return mLatitude; + } + + final void setElevation(double elevation) { + mElevation = elevation; + mHasElevation = true; + } + + public final boolean hasElevation() { + return mHasElevation; + } + + public final double getElevation() { + return mElevation; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java new file mode 100644 index 0000000..7fb37ce --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008 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.location; + +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Content provider to display {@link Track} objects in a Table. + * <p/>The expected type for the input is {@link Track}<code>[]</code>. + */ +public class TrackContentProvider implements IStructuredContentProvider { + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Track[]) { + return (Track[])inputElement; + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java new file mode 100644 index 0000000..81d1f7d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008 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.location; + +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Table; + +import java.util.Date; + +/** + * Label Provider for {@link Table} objects displaying {@link Track} objects. + */ +public class TrackLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof Track) { + Track track = (Track)element; + switch (columnIndex) { + case 0: + return track.getName(); + case 1: + return Integer.toString(track.getPointCount()); + case 2: + long time = track.getFirstPointTime(); + if (time != -1) { + return new Date(time).toString(); + } + break; + case 3: + time = track.getLastPointTime(); + if (time != -1) { + return new Date(time).toString(); + } + break; + case 4: + return track.getComment(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java new file mode 100644 index 0000000..527f4bf --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2008 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.location; + + +/** + * A Track Point. + * <p/>A track point is a point in time and space. + */ +public class TrackPoint extends LocationPoint { + private long mTime; + + void setTime(long time) { + mTime = time; + } + + public long getTime() { + return mTime; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java new file mode 100644 index 0000000..32880bd --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008 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.location; + +/** + * A GPS/KML way point. + * <p/>A waypoint is a user specified location, with a name and an optional description. + */ +public final class WayPoint extends LocationPoint { + private String mName; + private String mDescription; + + void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + void setDescription(String description) { + mDescription = description; + } + + public String getDescription() { + return mDescription; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java new file mode 100644 index 0000000..fced777 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 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.location; + +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Content provider to display {@link WayPoint} objects in a Table. + * <p/>The expected type for the input is {@link WayPoint}<code>[]</code>. + */ +public class WayPointContentProvider implements IStructuredContentProvider { + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof WayPoint[]) { + return (WayPoint[])inputElement; + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java new file mode 100644 index 0000000..f5e6f1b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2008 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.location; + +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Table; + +/** + * Label Provider for {@link Table} objects displaying {@link WayPoint} objects. + */ +public class WayPointLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)element; + switch (columnIndex) { + case 0: + return wayPoint.getName(); + case 1: + return String.format("%.6f", wayPoint.getLongitude()); + case 2: + return String.format("%.6f", wayPoint.getLatitude()); + case 3: + if (wayPoint.hasElevation()) { + return String.format("%.1f", wayPoint.getElevation()); + } else { + return "-"; + } + case 4: + return wayPoint.getDescription(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java new file mode 100644 index 0000000..9de1ac7 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008 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.log.event; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +public class BugReportImporter { + + private final static String TAG_HEADER = "------ EVENT LOG TAGS ------"; + private final static String LOG_HEADER = "------ EVENT LOG ------"; + private final static String HEADER_TAG = "------"; + + private String[] mTags; + private String[] mLog; + + public BugReportImporter(String filePath) throws FileNotFoundException { + BufferedReader reader = new BufferedReader( + new InputStreamReader(new FileInputStream(filePath))); + + try { + String line; + while ((line = reader.readLine()) != null) { + if (TAG_HEADER.equals(line)) { + readTags(reader); + return; + } + } + } catch (IOException e) { + } + } + + public String[] getTags() { + return mTags; + } + + public String[] getLog() { + return mLog; + } + + private void readTags(BufferedReader reader) throws IOException { + String line; + + ArrayList<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + if (LOG_HEADER.equals(line)) { + mTags = content.toArray(new String[content.size()]); + readLog(reader); + return; + } else { + content.add(line); + } + } + } + + private void readLog(BufferedReader reader) throws IOException { + String line; + + ArrayList<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + if (line.startsWith(HEADER_TAG) == false) { + content.add(line); + } else { + break; + } + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java new file mode 100644 index 0000000..473387a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; + +import java.util.ArrayList; + +public class DisplayFilteredLog extends DisplayLog { + + public DisplayFilteredLog(String name) { + super(name); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList<ValueDisplayDescriptor> valueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + addToLog(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_FILTERED_LOG; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java new file mode 100644 index 0000000..0cffd7e --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; +import org.jfree.chart.renderer.xy.XYAreaRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.Millisecond; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class DisplayGraph extends EventDisplay { + + public DisplayGraph(String name) { + super(name); + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + Collection<TimeSeriesCollection> datasets = mValueTypeDataSetMap.values(); + for (TimeSeriesCollection dataset : datasets) { + dataset.removeAllSeries(); + } + if (mOccurrenceDataSet != null) { + mOccurrenceDataSet.removeAllSeries(); + } + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + String title = getChartTitle(logParser); + return createCompositeChart(parent, logParser, title); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList<ValueDisplayDescriptor> valueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + updateChart(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined + * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from + * the two lists. + * <p/>This method is only called when at least one of the descriptor list is non empty. + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + private void updateChart(EventContainer event, EventLogParser logParser, + ArrayList<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) { + Map<Integer, String> tagMap = logParser.getTagMap(); + + Millisecond millisecondTime = null; + long msec = -1; + + // If the event container is a cpu container (tag == 2721), and there is no descriptor + // for the total CPU load, then we do accumulate all the values. + boolean accumulateValues = false; + double accumulatedValue = 0; + + if (event.mTag == 2721) { + accumulateValues = true; + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + accumulateValues &= (descriptor.valueIndex != 0); + } + } + + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + try { + // get the hashmap for this descriptor + HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap<Integer, TimeSeries>(); + mValueDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it + if (timeSeries == null) { + // get the series name + String seriesFullName = null; + String seriesLabel = getSeriesLabel(event, descriptor); + + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + seriesFullName = String.format("%1$s / %2$s", seriesLabel, + descriptor.valueName); + break; + case EVENT_CHECK_SAME_VALUE: + seriesFullName = String.format("%1$s", seriesLabel); + break; + default: + seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel, + tagMap.get(descriptor.eventTag), + descriptor.valueName); + break; + } + + // get the data set for this ValueType + TimeSeriesCollection dataset = getValueDataset( + logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex] + .getValueType(), + accumulateValues); + + // create the series + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000); + } + + dataset.addSeries(timeSeries); + + // add it to the map. + map.put(event.pid, timeSeries); + } + + // update the timeSeries. + + // get the value from the event + double value = event.getValueAsDouble(descriptor.valueIndex); + + // accumulate the values if needed. + if (accumulateValues) { + accumulatedValue += value; + value = accumulatedValue; + } + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, value); + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) { + try { + // get the hashmap for this descriptor + HashMap<Integer, TimeSeries> map = mOcurrenceDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap<Integer, TimeSeries>(); + mOcurrenceDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it. + if (timeSeries == null) { + String seriesLabel = getSeriesLabel(event, descriptor); + + String seriesFullName = String.format("[%1$s:%2$s]", + tagMap.get(descriptor.eventTag), seriesLabel); + + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge); + } + + getOccurrenceDataSet().addSeries(timeSeries); + + map.put(event.pid, timeSeries); + } + + // update the series + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + // go through all the series and remove old values. + if (msec != -1 && mMaximumChartItemAge != -1) { + Collection<HashMap<Integer, TimeSeries>> pidMapValues = + mValueDescriptorSeriesMap.values(); + + for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) { + Collection<TimeSeries> seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + + pidMapValues = mOcurrenceDescriptorSeriesMap.values(); + for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) { + Collection<TimeSeries> seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + } + } + + /** + * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}. + * If the data set is not yet created, it is first allocated and set up into the + * {@link org.jfree.chart.JFreeChart} object. + * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set. + * @param accumulateValues + */ + private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) { + TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type); + if (dataset == null) { + // create the data set and store it in the map + dataset = new TimeSeriesCollection(); + mValueTypeDataSetMap.put(type, dataset); + + // create the renderer and configure it depending on the ValueType + AbstractXYItemRenderer renderer; + if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) { + renderer = new XYAreaRenderer(); + } else { + XYLineAndShapeRenderer r = new XYLineAndShapeRenderer(); + r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT); + + renderer = r; + } + + // set both the dataset and the renderer in the plot object. + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, dataset); + xyPlot.setRenderer(mDataSetCount, renderer); + + // put a new axis label, and configure it. + NumberAxis axis = new NumberAxis(type.toString()); + + if (type == EventValueDescription.ValueType.PERCENT) { + // force percent range to be (0,100) fixed. + axis.setAutoRange(false); + axis.setRange(0., 100.); + } + + // for the index, we ignore the occurrence dataset + int count = mDataSetCount; + if (mOccurrenceDataSet != null) { + count--; + } + + xyPlot.setRangeAxis(count, axis); + if ((count % 2) == 0) { + xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT); + } else { + xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT); + } + + // now we link the dataset and the axis + xyPlot.mapDatasetToRangeAxis(mDataSetCount, count); + + mDataSetCount++; + } + + return dataset; + } + + /** + * Return the series label for this event. This only contains the pid information. + * @param event the {@link EventContainer} + * @param descriptor the {@link OccurrenceDisplayDescriptor} + * @return the series label. + * @throws InvalidTypeException + */ + private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor) + throws InvalidTypeException { + if (descriptor.seriesValueIndex != -1) { + if (descriptor.includePid == false) { + return event.getValueAsString(descriptor.seriesValueIndex); + } else { + return String.format("%1$s (%2$d)", + event.getValueAsString(descriptor.seriesValueIndex), event.pid); + } + } + + return Integer.toString(event.pid); + } + + /** + * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not + * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object. + */ + private TimeSeriesCollection getOccurrenceDataSet() { + if (mOccurrenceDataSet == null) { + mOccurrenceDataSet = new TimeSeriesCollection(); + + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet); + + OccurrenceRenderer renderer = new OccurrenceRenderer(); + renderer.setBaseShapesVisible(false); + xyPlot.setRenderer(mDataSetCount, renderer); + + mDataSetCount++; + } + + return mOccurrenceDataSet; + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_GRAPH; + } + + /** + * Sets the current {@link EventLogParser} object. + */ + @Override + protected void setNewLogParser(EventLogParser logParser) { + if (mChart != null) { + mChart.setTitle(getChartTitle(logParser)); + } + } + /** + * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}. + * + * @param logParser the logParser. + * @return the chart title. + */ + private String getChartTitle(EventLogParser logParser) { + if (mValueDescriptors.size() > 0) { + String chartDesc = null; + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + if (logParser != null) { + chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag); + } + break; + case EVENT_CHECK_SAME_VALUE: + if (logParser != null) { + chartDesc = String.format("%1$s / %2$s", + logParser.getTagMap().get(mValueDescriptors.get(0).eventTag), + mValueDescriptors.get(0).valueName); + } + break; + } + + if (chartDesc != null) { + return String.format("%1$s - %2$s", mName, chartDesc); + } + } + + return mName; + } +}
\ No newline at end of file diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java new file mode 100644 index 0000000..26296f3 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.InvalidTypeException; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.TableHelper; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.Calendar; + +public class DisplayLog extends EventDisplay { + public DisplayLog(String name) { + super(name); + } + + private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$ + private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$ + private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$ + private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$ + private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$ + private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$ + + /** + * Resets the display. + */ + @Override + void resetUI() { + mLogTable.removeAll(); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + addToLog(event, logParser); + } + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + Control createComposite(Composite parent, EventLogParser logParser, ILogColumnListener listener) { + return createLogUI(parent, listener); + } + + /** + * Adds an {@link EventContainer} to the log. + * + * @param event the event. + * @param logParser the log parser. + */ + private void addToLog(EventContainer event, EventLogParser logParser) { + ScrollBar bar = mLogTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // get the date. + Calendar c = Calendar.getInstance(); + long msec = (long) event.sec * 1000L; + c.setTimeInMillis(msec); + + // convert the time into a string + String date = String.format("%1$tF %1$tT", c); + + String eventName = logParser.getTagMap().get(event.mTag); + String pidName = Integer.toString(event.pid); + + // get the value description + EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag); + if (valueDescription != null) { + for (int i = 0; i < valueDescription.length; i++) { + EventValueDescription description = valueDescription[i]; + try { + String value = event.getValueAsString(i); + + logValue(date, pidName, eventName, description.getName(), value, + description.getEventValueType(), description.getValueType()); + } catch (InvalidTypeException e) { + logValue(date, pidName, eventName, description.getName(), e.getMessage(), + description.getEventValueType(), description.getValueType()); + } + } + + // scroll if needed, by showing the last item + if (scroll) { + int itemCount = mLogTable.getItemCount(); + if (itemCount > 0) { + mLogTable.showItem(mLogTable.getItem(itemCount - 1)); + } + } + } + } + + /** + * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by + * the list of descriptors. If an event is configured to be displayed by value and occurrence, + * only the values are displayed (as they mark an event occurrence anyway). + * <p/>This method is only called when at least one of the descriptor list is non empty. + * + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + protected void addToLog(EventContainer event, EventLogParser logParser, + ArrayList<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) { + ScrollBar bar = mLogTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // get the date. + Calendar c = Calendar.getInstance(); + long msec = (long) event.sec * 1000L; + c.setTimeInMillis(msec); + + // convert the time into a string + String date = String.format("%1$tF %1$tT", c); + + String eventName = logParser.getTagMap().get(event.mTag); + String pidName = Integer.toString(event.pid); + + if (valueDescriptors.size() > 0) { + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + logDescriptor(event, descriptor, date, pidName, eventName, logParser); + } + } else { + // we display the event. Since the StringBuilder contains the header (date, event name, + // pid) at this point, there isn't anything else to display. + } + + // scroll if needed, by showing the last item + if (scroll) { + int itemCount = mLogTable.getItemCount(); + if (itemCount > 0) { + mLogTable.showItem(mLogTable.getItem(itemCount - 1)); + } + } + } + + + /** + * Logs a value in the ui. + * + * @param date + * @param pid + * @param event + * @param valueName + * @param value + * @param eventValueType + * @param valueType + */ + private void logValue(String date, String pid, String event, String valueName, + String value, EventContainer.EventValueType eventValueType, EventValueDescription.ValueType valueType) { + + TableItem item = new TableItem(mLogTable, SWT.NONE); + item.setText(0, date); + item.setText(1, pid); + item.setText(2, event); + item.setText(3, valueName); + item.setText(4, value); + + String type; + if (valueType != EventValueDescription.ValueType.NOT_APPLICABLE) { + type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString()); + } else { + type = eventValueType.toString(); + } + + item.setText(5, type); + } + + /** + * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}. + * + * @param event the EventContainer + * @param descriptor the ValueDisplayDescriptor defining which value to display. + * @param date the date of the event in a string. + * @param pidName + * @param eventName + * @param logParser + */ + private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor, + String date, String pidName, String eventName, EventLogParser logParser) { + + String value; + try { + value = event.getValueAsString(descriptor.valueIndex); + } catch (InvalidTypeException e) { + value = e.getMessage(); + } + + EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag); + + EventValueDescription valueDescription = values[descriptor.valueIndex]; + + logValue(date, pidName, eventName, descriptor.valueName, value, + valueDescription.getEventValueType(), valueDescription.getValueType()); + } + + /** + * Creates the UI for a log display. + * + * @param parent the parent {@link Composite} + * @param listener the {@link ILogColumnListener} to notify on column resize events. + * @return the top Composite of the UI. + */ + private Control createLogUI(Composite parent, final ILogColumnListener listener) { + Composite mainComp = new Composite(parent, SWT.NONE); + GridLayout gl; + mainComp.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + mainComp.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mLogTable = null; + } + }); + + Label l = new Label(mainComp, SWT.CENTER); + l.setText(mName); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL | + SWT.BORDER); + mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + TableColumn col = TableHelper.createTableColumn( + mLogTable, "Time", + SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(0, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "pid", + SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(1, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Event", + SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(2, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Name", + SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(3, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Value", + SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(4, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Type", + SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(5, (TableColumn) source); + } + } + }); + + mLogTable.setHeaderVisible(true); + mLogTable.setLinesVisible(true); + + return mainComp; + } + + /** + * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable). + * <p/> + * This does nothing if the <code>Table</code> object is <code>null</code> (because the display + * type does not use a column) or if the <code>index</code>-th column is in fact the originating + * column passed as argument. + * + * @param index the index of the column to resize + * @param sourceColumn the original column that was resize, and on which we need to sync the + * index-th column width. + */ + @Override + void resizeColumn(int index, TableColumn sourceColumn) { + if (mLogTable != null) { + TableColumn col = mLogTable.getColumn(index); + if (col != sourceColumn) { + col.setWidth(sourceColumn.getWidth()); + } + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_LOG_ALL; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java new file mode 100644 index 0000000..82cc7a4 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.FixedMillisecond; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.util.ShapeUtilities; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class DisplaySync extends SyncCommon { + + // Information to graph for each authority + private TimePeriodValues mDatasetsSync[]; + private List<String> mTooltipsSync[]; + private CustomXYToolTipGenerator mTooltipGenerators[]; + private TimeSeries mDatasetsSyncTickle[]; + + // Dataset of error events to graph + private TimeSeries mDatasetError; + + public DisplaySync(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Status"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + + XYBarRenderer br = new XYBarRenderer(); + mDatasetsSync = new TimePeriodValues[NUM_AUTHS]; + mTooltipsSync = new List[NUM_AUTHS]; + mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS]; + + TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(0, br); + + XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer(); + ls.setBaseLinesVisible(false); + mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS]; + TimeSeriesCollection tsc = new TimeSeriesCollection(); + xyPlot.setDataset(1, tsc); + xyPlot.setRenderer(1, ls); + + mDatasetError = new TimeSeries("Errors", FixedMillisecond.class); + xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError)); + XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer(); + errls.setBaseLinesVisible(false); + errls.setSeriesPaint(0, Color.RED); + xyPlot.setRenderer(2, errls); + + for (int i = 0; i < NUM_AUTHS; i++) { + br.setSeriesPaint(i, AUTH_COLORS[i]); + ls.setSeriesPaint(i, AUTH_COLORS[i]); + mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]); + tpvc.addSeries(mDatasetsSync[i]); + mTooltipsSync[i] = new ArrayList<String>(); + mTooltipGenerators[i] = new CustomXYToolTipGenerator(); + br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]); + mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]); + + mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle", + FixedMillisecond.class); + tsc.addSeries(mDatasetsSyncTickle[i]); + ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f)); + } + } + + /** + * Updates the display with a new event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + super.newEvent(event, logParser); // Handle sync operation + try { + if (event.mTag == EVENT_TICKLE) { + int auth = getAuth(event.getValueAsString(0)); + if (auth >= 0) { + long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1); + } + } + } catch (InvalidTypeException e) { + } + } + + /** + * Generate the height for an event. + * Height is somewhat arbitrarily the count of "things" that happened + * during the sync. + * When network traffic measurements are available, code should be modified + * to use that instead. + * @param details The details string associated with the event + * @return The height in arbirary units (0-100) + */ + private int getHeightFromDetails(String details) { + if (details == null) { + return 1; // Arbitrary + } + int total = 0; + String parts[] = details.split("[a-zA-Z]"); + for (String part : parts) { + if ("".equals(part)) continue; + total += Integer.parseInt(part); + } + if (total == 0) { + total = 1; + } + return total; + } + + /** + * Generates the tooltips text for an event. + * This method decodes the cryptic details string. + * @param auth The authority associated with the event + * @param details The details string + * @param eventSource server, poll, etc. + * @return The text to display in the tooltips + */ + private String getTextFromDetails(int auth, String details, int eventSource) { + + StringBuffer sb = new StringBuffer(); + sb.append(AUTH_NAMES[auth]).append(": \n"); + + Scanner scanner = new Scanner(details); + Pattern charPat = Pattern.compile("[a-zA-Z]"); + Pattern numPat = Pattern.compile("[0-9]+"); + while (scanner.hasNext()) { + String key = scanner.findInLine(charPat); + int val = Integer.parseInt(scanner.findInLine(numPat)); + if (auth == GMAIL && "M".equals(key)) { + sb.append("messages from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "L".equals(key)) { + sb.append("labels from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "C".equals(key)) { + sb.append("check conversation requests from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "A".equals(key)) { + sb.append("attachments from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "U".equals(key)) { + sb.append("op updates from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "u".equals(key)) { + sb.append("op updates to server: ").append(val).append("\n"); + } else if (auth == GMAIL && "S".equals(key)) { + sb.append("send/receive cycles: ").append(val).append("\n"); + } else if ("Q".equals(key)) { + sb.append("queries to server: ").append(val).append("\n"); + } else if ("E".equals(key)) { + sb.append("entries from server: ").append(val).append("\n"); + } else if ("u".equals(key)) { + sb.append("updates from client: ").append(val).append("\n"); + } else if ("i".equals(key)) { + sb.append("inserts from client: ").append(val).append("\n"); + } else if ("d".equals(key)) { + sb.append("deletes from client: ").append(val).append("\n"); + } else if ("f".equals(key)) { + sb.append("full sync requested\n"); + } else if ("r".equals(key)) { + sb.append("partial sync unavailable\n"); + } else if ("X".equals(key)) { + sb.append("hard error\n"); + } else if ("e".equals(key)) { + sb.append("number of parse exceptions: ").append(val).append("\n"); + } else if ("c".equals(key)) { + sb.append("number of conflicts: ").append(val).append("\n"); + } else if ("a".equals(key)) { + sb.append("number of auth exceptions: ").append(val).append("\n"); + } else if ("D".equals(key)) { + sb.append("too many deletions\n"); + } else if ("R".equals(key)) { + sb.append("too many retries: ").append(val).append("\n"); + } else if ("b".equals(key)) { + sb.append("database error\n"); + } else if ("x".equals(key)) { + sb.append("soft error\n"); + } else if ("l".equals(key)) { + sb.append("sync already in progress\n"); + } else if ("I".equals(key)) { + sb.append("io exception\n"); + } else if (auth == CONTACTS && "p".equals(key)) { + sb.append("photos uploaded from client: ").append(val).append("\n"); + } else if (auth == CONTACTS && "P".equals(key)) { + sb.append("photos downloaded from server: ").append(val).append("\n"); + } else if (auth == CALENDAR && "F".equals(key)) { + sb.append("server refresh\n"); + } else if (auth == CALENDAR && "s".equals(key)) { + sb.append("server diffs fetched\n"); + } else { + sb.append(key).append("=").append(val); + } + } + if (eventSource == 0) { + sb.append("(server)"); + } else if (eventSource == 1) { + sb.append("(local)"); + } else if (eventSource == 2) { + sb.append("(poll)"); + } else if (eventSource == 3) { + sb.append("(user)"); + } + return sb.toString(); + } + + + /** + * Callback to process a sync event. + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (!newEvent) { + // Details arrived for a previous sync event + // Remove event before reinserting. + int lastItem = mDatasetsSync[auth].getItemCount(); + mDatasetsSync[auth].delete(lastItem-1, lastItem-1); + mTooltipsSync[auth].remove(lastItem-1); + } + double height = getHeightFromDetails(details); + height = height / (stopTime - startTime + 1) * 10000; + if (height > 30) { + height = 30; + } + mDatasetsSync[auth].add(new SimpleTimePeriod(startTime, stopTime), height); + mTooltipsSync[auth].add(getTextFromDetails(auth, details, syncSource)); + mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]); + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC; + } +}
\ No newline at end of file diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java new file mode 100644 index 0000000..36d90ce --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.data.time.RegularTimePeriod; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; + +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +public class DisplaySyncHistogram extends SyncCommon { + + Map<SimpleTimePeriod, Integer> mTimePeriodMap[]; + + // Information to graph for each authority + private TimePeriodValues mDatasetsSyncHist[]; + + public DisplaySyncHistogram(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Histogram"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + + AbstractXYItemRenderer br = new XYBarRenderer(); + mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1]; + mTimePeriodMap = new HashMap[NUM_AUTHS + 1]; + + TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(br); + + for (int i = 0; i < NUM_AUTHS + 1; i++) { + br.setSeriesPaint(i, AUTH_COLORS[i]); + mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]); + tpvc.addSeries(mDatasetsSyncHist[i]); + mTimePeriodMap[i] = new HashMap<SimpleTimePeriod, Integer>(); + + } + } + + /** + * Callback to process a sync event. + * + * @param event The sync event + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (newEvent) { + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + auth = ERRORS; + } + double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour + addHistEvent(0, auth, delta); + } else { + // sync_details arrived for an event that has already been graphed. + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + // Item turns out to be in error, so transfer time from old auth to error. + double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour + addHistEvent(0, auth, -delta); + addHistEvent(0, ERRORS, delta); + } + } + } + + /** + * Helper to add an event to the data series. + * Also updates error series if appropriate (x or X in details). + * @param stopTime Time event ends + * @param auth Sync authority + * @param value Value to graph for event + */ + private void addHistEvent(long stopTime, int auth, double value) { + SimpleTimePeriod hour = getTimePeriod(stopTime, mHistWidth); + + // Loop over all datasets to do the stacking. + for (int i = auth; i <= ERRORS; i++) { + addToPeriod(mDatasetsSyncHist, i, hour, value); + } + } + + private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period, + double value) { + int index; + if (mTimePeriodMap[auth].containsKey(period)) { + index = mTimePeriodMap[auth].get(period); + double oldValue = tpv[auth].getValue(index).doubleValue(); + tpv[auth].update(index, oldValue + value); + } else { + index = tpv[auth].getItemCount(); + mTimePeriodMap[auth].put(period, index); + tpv[auth].add(period, value); + } + } + + /** + * Creates a multiple-hour time period for the histogram. + * @param time Time in milliseconds. + * @param numHoursWide: should divide into a day. + * @return SimpleTimePeriod covering the number of hours and containing time. + */ + private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) { + Date date = new Date(time); + TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE; + Calendar calendar = Calendar.getInstance(zone); + calendar.setTime(date); + long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) + + calendar.get(Calendar.DAY_OF_YEAR) * 24; + int year = calendar.get(Calendar.YEAR); + hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide; + calendar.clear(); + calendar.set(year, 0, 1, 0, 0); // Jan 1 + long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000; + return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000); + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC_HIST; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java new file mode 100644 index 0000000..9ce7045 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2009 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.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +public class DisplaySyncPerf extends SyncCommon { + + CustomXYToolTipGenerator mTooltipGenerator; + List mTooltips[]; + + // The series number for each graphed item. + // sync authorities are 0-3 + private static final int DB_QUERY = 4; + private static final int DB_WRITE = 5; + private static final int HTTP_NETWORK = 6; + private static final int HTTP_PROCESSING = 7; + private static final int NUM_SERIES = (HTTP_PROCESSING + 1); + private static final String SERIES_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", + "DB Query", "DB Write", "HTTP Response", "HTTP Processing",}; + private static final Color SERIES_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, + Color.ORANGE, Color.RED, Color.CYAN, Color.PINK, Color.DARK_GRAY}; + private static final double SERIES_YCOORD[] = {0, 0, 0, 0, 1, 1, 2, 2}; + + // Values from data/etc/event-log-tags + private static final int EVENT_DB_OPERATION = 52000; + private static final int EVENT_HTTP_STATS = 52001; + // op types for EVENT_DB_OPERATION + final int EVENT_DB_QUERY = 0; + final int EVENT_DB_WRITE = 1; + + // Information to graph for each authority + private TimePeriodValues mDatasets[]; + + /** + * TimePeriodValuesCollection that supports Y intervals. This allows the + * creation of "floating" bars, rather than bars rooted to the axis. + */ + class YIntervalTimePeriodValuesCollection extends TimePeriodValuesCollection { + /** default serial UID */ + private static final long serialVersionUID = 1L; + + private double yheight; + + /** + * Constructs a collection of bars with a fixed Y height. + * + * @param yheight The height of the bars. + */ + YIntervalTimePeriodValuesCollection(double yheight) { + this.yheight = yheight; + } + + /** + * Returns ending Y value that is a fixed amount greater than the starting value. + * + * @param series the series (zero-based index). + * @param item the item (zero-based index). + * @return The ending Y value for the specified series and item. + */ + @Override + public Number getEndY(int series, int item) { + return getY(series, item).doubleValue() + yheight; + } + } + + /** + * Constructs a graph of network and database stats. + * + * @param name The name of this graph in the graph list. + */ + public DisplaySyncPerf(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Performance"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.getRangeAxis().setVisible(false); + mTooltipGenerator = new CustomXYToolTipGenerator(); + mTooltips = new List[NUM_SERIES]; + + XYBarRenderer br = new XYBarRenderer(); + br.setUseYInterval(true); + mDatasets = new TimePeriodValues[NUM_SERIES]; + + TimePeriodValuesCollection tpvc = new YIntervalTimePeriodValuesCollection(1); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(br); + + for (int i = 0; i < NUM_SERIES; i++) { + br.setSeriesPaint(i, SERIES_COLORS[i]); + mDatasets[i] = new TimePeriodValues(SERIES_NAMES[i]); + tpvc.addSeries(mDatasets[i]); + mTooltips[i] = new ArrayList<String>(); + mTooltipGenerator.addToolTipSeries(mTooltips[i]); + br.setSeriesToolTipGenerator(i, mTooltipGenerator); + } + } + + /** + * Updates the display with a new event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + super.newEvent(event, logParser); // Handle sync operation + try { + if (event.mTag == EVENT_DB_OPERATION) { + // 52000 db_operation (name|3),(op_type|1|5),(time|2|3) + String tip = event.getValueAsString(0); + long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + int opType = Integer.parseInt(event.getValueAsString(1)); + long duration = Long.parseLong(event.getValueAsString(2)); + + if (opType == EVENT_DB_QUERY) { + mDatasets[DB_QUERY].add(new SimpleTimePeriod(endTime - duration, endTime), + SERIES_YCOORD[DB_QUERY]); + mTooltips[DB_QUERY].add(tip); + } else if (opType == EVENT_DB_WRITE) { + mDatasets[DB_WRITE].add(new SimpleTimePeriod(endTime - duration, endTime), + SERIES_YCOORD[DB_WRITE]); + mTooltips[DB_WRITE].add(tip); + } + } else if (event.mTag == EVENT_HTTP_STATS) { + // 52001 http_stats (useragent|3),(response|2|3),(processing|2|3),(tx|1|2),(rx|1|2) + String tip = event.getValueAsString(0) + ", tx:" + event.getValueAsString(3) + + ", rx: " + event.getValueAsString(4); + long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + long netEndTime = endTime - Long.parseLong(event.getValueAsString(2)); + long netStartTime = netEndTime - Long.parseLong(event.getValueAsString(1)); + mDatasets[HTTP_NETWORK].add(new SimpleTimePeriod(netStartTime, netEndTime), + SERIES_YCOORD[HTTP_NETWORK]); + mDatasets[HTTP_PROCESSING].add(new SimpleTimePeriod(netEndTime, endTime), + SERIES_YCOORD[HTTP_PROCESSING]); + mTooltips[HTTP_NETWORK].add(tip); + mTooltips[HTTP_PROCESSING].add(tip); + } + } catch (InvalidTypeException e) { + } + } + + /** + * Callback from super.newEvent to process a sync event. + * + * @param event The sync event + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (newEvent) { + mDatasets[auth].add(new SimpleTimePeriod(startTime, stopTime), SERIES_YCOORD[auth]); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC_PERF; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java new file mode 100644 index 0000000..2223a4d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java @@ -0,0 +1,971 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.Log; +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventContainer.CompareMethod; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.event.ChartChangeEvent; +import org.jfree.chart.event.ChartChangeEventType; +import org.jfree.chart.event.ChartChangeListener; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.time.Millisecond; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.experimental.chart.swt.ChartComposite; +import org.jfree.experimental.swt.SWTUtils; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Represents a custom display of one or more events. + */ +abstract class EventDisplay { + + private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$ + private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$ + private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$ + private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$ + + private final static String FILTER_VALUE_NULL = "<null>"; //$NON-NLS-1$ + + public final static int DISPLAY_TYPE_LOG_ALL = 0; + public final static int DISPLAY_TYPE_FILTERED_LOG = 1; + public final static int DISPLAY_TYPE_GRAPH = 2; + public final static int DISPLAY_TYPE_SYNC = 3; + public final static int DISPLAY_TYPE_SYNC_HIST = 4; + public final static int DISPLAY_TYPE_SYNC_PERF = 5; + + private final static int EVENT_CHECK_FAILED = 0; + protected final static int EVENT_CHECK_SAME_TAG = 1; + protected final static int EVENT_CHECK_SAME_VALUE = 2; + + /** + * Creates the appropriate EventDisplay subclass. + * + * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc) + * @param name the name of the display + * @return the created object + */ + public static EventDisplay eventDisplayFactory(int type, String name) { + switch (type) { + case DISPLAY_TYPE_LOG_ALL: + return new DisplayLog(name); + case DISPLAY_TYPE_FILTERED_LOG: + return new DisplayFilteredLog(name); + case DISPLAY_TYPE_SYNC: + return new DisplaySync(name); + case DISPLAY_TYPE_SYNC_HIST: + return new DisplaySyncHistogram(name); + case DISPLAY_TYPE_GRAPH: + return new DisplayGraph(name); + case DISPLAY_TYPE_SYNC_PERF: + return new DisplaySyncPerf(name); + default: + throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$ + } + } + + /** + * Adds event to the display. + * @param event The event + * @param logParser The log parser. + */ + abstract void newEvent(EventContainer event, EventLogParser logParser); + + /** + * Resets the display. + */ + abstract void resetUI(); + + /** + * Gets display type + * + * @return display type as an integer + */ + abstract int getDisplayType(); + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + abstract Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener); + + interface ILogColumnListener { + void columnResized(int index, TableColumn sourceColumn); + } + + /** + * Describes an event to be displayed. + */ + static class OccurrenceDisplayDescriptor { + + int eventTag = -1; + int seriesValueIndex = -1; + boolean includePid = false; + int filterValueIndex = -1; + CompareMethod filterCompareMethod = CompareMethod.EQUAL_TO; + Object filterValue = null; + + OccurrenceDisplayDescriptor() { + } + + OccurrenceDisplayDescriptor(OccurrenceDisplayDescriptor descriptor) { + replaceWith(descriptor); + } + + OccurrenceDisplayDescriptor(int eventTag) { + this.eventTag = eventTag; + } + + OccurrenceDisplayDescriptor(int eventTag, int seriesValueIndex) { + this.eventTag = eventTag; + this.seriesValueIndex = seriesValueIndex; + } + + void replaceWith(OccurrenceDisplayDescriptor descriptor) { + eventTag = descriptor.eventTag; + seriesValueIndex = descriptor.seriesValueIndex; + includePid = descriptor.includePid; + filterValueIndex = descriptor.filterValueIndex; + filterCompareMethod = descriptor.filterCompareMethod; + filterValue = descriptor.filterValue; + } + + /** + * Loads the descriptor parameter from a storage string. The storage string must have + * been generated with {@link #getStorageString()}. + * + * @param storageString the storage string + */ + final void loadFrom(String storageString) { + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR)); + loadFrom(values, 0); + } + + /** + * Loads the parameters from an array of strings. + * + * @param storageStrings the strings representing each parameter. + * @param index the starting index in the array of strings. + * @return the new index in the array. + */ + protected int loadFrom(String[] storageStrings, int index) { + eventTag = Integer.parseInt(storageStrings[index++]); + seriesValueIndex = Integer.parseInt(storageStrings[index++]); + includePid = Boolean.parseBoolean(storageStrings[index++]); + filterValueIndex = Integer.parseInt(storageStrings[index++]); + try { + filterCompareMethod = CompareMethod.valueOf(storageStrings[index++]); + } catch (IllegalArgumentException e) { + // if the name does not match any known CompareMethod, we init it to the default one + filterCompareMethod = CompareMethod.EQUAL_TO; + } + String value = storageStrings[index++]; + if (filterValueIndex != -1 && FILTER_VALUE_NULL.equals(value) == false) { + filterValue = EventValueType.getObjectFromStorageString(value); + } + + return index; + } + + /** + * Returns the storage string for the receiver. + */ + String getStorageString() { + StringBuilder sb = new StringBuilder(); + sb.append(eventTag); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(seriesValueIndex); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(Boolean.toString(includePid)); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(filterValueIndex); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(filterCompareMethod.name()); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + if (filterValue != null) { + String value = EventValueType.getStorageString(filterValue); + if (value != null) { + sb.append(value); + } else { + sb.append(FILTER_VALUE_NULL); + } + } else { + sb.append(FILTER_VALUE_NULL); + } + + return sb.toString(); + } + } + + /** + * Describes an event value to be displayed. + */ + static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor { + String valueName; + int valueIndex = -1; + + ValueDisplayDescriptor() { + super(); + } + + ValueDisplayDescriptor(ValueDisplayDescriptor descriptor) { + super(); + replaceWith(descriptor); + } + + ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex) { + super(eventTag); + this.valueName = valueName; + this.valueIndex = valueIndex; + } + + ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex, + int seriesValueIndex) { + super(eventTag, seriesValueIndex); + this.valueName = valueName; + this.valueIndex = valueIndex; + } + + @Override + void replaceWith(OccurrenceDisplayDescriptor descriptor) { + super.replaceWith(descriptor); + if (descriptor instanceof ValueDisplayDescriptor) { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor; + valueName = valueDescriptor.valueName; + valueIndex = valueDescriptor.valueIndex; + } + } + + /** + * Loads the parameters from an array of strings. + * + * @param storageStrings the strings representing each parameter. + * @param index the starting index in the array of strings. + * @return the new index in the array. + */ + @Override + protected int loadFrom(String[] storageStrings, int index) { + index = super.loadFrom(storageStrings, index); + valueName = storageStrings[index++]; + valueIndex = Integer.parseInt(storageStrings[index++]); + return index; + } + + /** + * Returns the storage string for the receiver. + */ + @Override + String getStorageString() { + String superStorage = super.getStorageString(); + + StringBuilder sb = new StringBuilder(); + sb.append(superStorage); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(valueName); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(valueIndex); + + return sb.toString(); + } + } + + /* ================== + * Event Display parameters. + * ================== */ + protected String mName; + + private boolean mPidFiltering = false; + + private ArrayList<Integer> mPidFilterList = null; + + protected final ArrayList<ValueDisplayDescriptor> mValueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + private final ArrayList<OccurrenceDisplayDescriptor> mOccurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + /* ================== + * Event Display members for display purpose. + * ================== */ + // chart objects + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>> mValueDescriptorSeriesMap = + new HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>>(); + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>> mOcurrenceDescriptorSeriesMap = + new HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>>(); + + /** + * This is a map of (ValueType, dataset) + */ + protected final HashMap<ValueType, TimeSeriesCollection> mValueTypeDataSetMap = + new HashMap<ValueType, TimeSeriesCollection>(); + + protected JFreeChart mChart; + protected TimeSeriesCollection mOccurrenceDataSet; + protected int mDataSetCount; + private ChartComposite mChartComposite; + protected long mMaximumChartItemAge = -1; + protected long mHistWidth = 1; + + // log objects. + protected Table mLogTable; + + /* ================== + * Misc data. + * ================== */ + protected int mValueDescriptorCheck = EVENT_CHECK_FAILED; + + EventDisplay(String name) { + mName = name; + } + + static EventDisplay clone(EventDisplay from) { + EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName()); + ed.mName = from.mName; + ed.mPidFiltering = from.mPidFiltering; + ed.mMaximumChartItemAge = from.mMaximumChartItemAge; + ed.mHistWidth = from.mHistWidth; + + if (from.mPidFilterList != null) { + ed.mPidFilterList = new ArrayList<Integer>(); + ed.mPidFilterList.addAll(from.mPidFilterList); + } + + for (ValueDisplayDescriptor desc : from.mValueDescriptors) { + ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc)); + } + ed.mValueDescriptorCheck = from.mValueDescriptorCheck; + + for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) { + ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc)); + } + return ed; + } + + /** + * Returns the parameters of the receiver as a single String for storage. + */ + String getStorageString() { + StringBuilder sb = new StringBuilder(); + + sb.append(mName); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDisplayType()); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(Boolean.toString(mPidFiltering)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getPidStorageString()); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDescriptorStorageString(mValueDescriptors)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDescriptorStorageString(mOccurrenceDescriptors)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(mMaximumChartItemAge); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(mHistWidth); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + + return sb.toString(); + } + + void setName(String name) { + mName = name; + } + + String getName() { + return mName; + } + + void setPidFiltering(boolean filterByPid) { + mPidFiltering = filterByPid; + } + + boolean getPidFiltering() { + return mPidFiltering; + } + + void setPidFilterList(ArrayList<Integer> pids) { + if (mPidFiltering == false) { + new InvalidParameterException(); + } + + mPidFilterList = pids; + } + + ArrayList<Integer> getPidFilterList() { + return mPidFilterList; + } + + void addPidFiler(int pid) { + if (mPidFiltering == false) { + new InvalidParameterException(); + } + + if (mPidFilterList == null) { + mPidFilterList = new ArrayList<Integer>(); + } + + mPidFilterList.add(pid); + } + + /** + * Returns an iterator to the list of {@link ValueDisplayDescriptor}. + */ + Iterator<ValueDisplayDescriptor> getValueDescriptors() { + return mValueDescriptors.iterator(); + } + + /** + * Update checks on the descriptors. Must be called whenever a descriptor is modified outside + * of this class. + */ + void updateValueDescriptorCheck() { + mValueDescriptorCheck = checkDescriptors(); + } + + /** + * Returns an iterator to the list of {@link OccurrenceDisplayDescriptor}. + */ + Iterator<OccurrenceDisplayDescriptor> getOccurrenceDescriptors() { + return mOccurrenceDescriptors.iterator(); + } + + /** + * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a + * {@link ValueDisplayDescriptor}. + * + * @param descriptor the descriptor to be added. + */ + void addDescriptor(OccurrenceDisplayDescriptor descriptor) { + if (descriptor instanceof ValueDisplayDescriptor) { + mValueDescriptors.add((ValueDisplayDescriptor) descriptor); + mValueDescriptorCheck = checkDescriptors(); + } else { + mOccurrenceDescriptors.add(descriptor); + } + } + + /** + * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}). + * + * @param descriptorClass the class of the descriptor to return. + * @param index the index of the descriptor to return. + * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor} + * or <code>null</code> if <code>descriptorClass</code> is another class. + */ + OccurrenceDisplayDescriptor getDescriptor( + Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) { + + if (descriptorClass == OccurrenceDisplayDescriptor.class) { + return mOccurrenceDescriptors.get(index); + } else if (descriptorClass == ValueDisplayDescriptor.class) { + return mValueDescriptors.get(index); + } + + return null; + } + + /** + * Removes a descriptor based on its class and index. + * + * @param descriptorClass the class of the descriptor. + * @param index the index of the descriptor to be removed. + */ + void removeDescriptor(Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) { + if (descriptorClass == OccurrenceDisplayDescriptor.class) { + mOccurrenceDescriptors.remove(index); + } else if (descriptorClass == ValueDisplayDescriptor.class) { + mValueDescriptors.remove(index); + mValueDescriptorCheck = checkDescriptors(); + } + } + + Control createCompositeChart(final Composite parent, EventLogParser logParser, + String title) { + mChart = ChartFactory.createTimeSeriesChart( + null, + null /* timeAxisLabel */, + null /* valueAxisLabel */, + null, /* dataset. set below */ + true /* legend */, + false /* tooltips */, + false /* urls */); + + // get the font to make a proper title. We need to convert the swt font, + // into an awt font. + Font f = parent.getFont(); + FontData[] fData = f.getFontData(); + + // event though on Mac OS there could be more than one fontData, we'll only use + // the first one. + FontData firstFontData = fData[0]; + + java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(), + firstFontData, true /* ensureSameSize */); + + + mChart.setTitle(new TextTitle(title, awtFont)); + + final XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setRangeCrosshairVisible(true); + xyPlot.setRangeCrosshairLockedOnData(true); + xyPlot.setDomainCrosshairVisible(true); + xyPlot.setDomainCrosshairLockedOnData(true); + + mChart.addChangeListener(new ChartChangeListener() { + public void chartChanged(ChartChangeEvent event) { + ChartChangeEventType type = event.getType(); + if (type == ChartChangeEventType.GENERAL) { + // because the value we need (rangeCrosshair and domainCrosshair) are + // updated on the draw, but the notification happens before the draw, + // we process the click in a future runnable! + parent.getDisplay().asyncExec(new Runnable() { + public void run() { + processClick(xyPlot); + } + }); + } + } + }); + + mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, // max draw width. We don't want it to zoom, so we put a big number + 3000, // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + true, // zoom + true); // tooltips + + mChartComposite.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mValueTypeDataSetMap.clear(); + mDataSetCount = 0; + mOccurrenceDataSet = null; + mChart = null; + mChartComposite = null; + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + }); + + return mChartComposite; + + } + + private void processClick(XYPlot xyPlot) { + double rangeValue = xyPlot.getRangeCrosshairValue(); + if (rangeValue != 0) { + double domainValue = xyPlot.getDomainCrosshairValue(); + + Millisecond msec = new Millisecond(new Date((long) domainValue)); + + // look for values in the dataset that contains data at this TimePeriod + Set<ValueDisplayDescriptor> descKeys = mValueDescriptorSeriesMap.keySet(); + + for (ValueDisplayDescriptor descKey : descKeys) { + HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descKey); + + Set<Integer> pidKeys = map.keySet(); + + for (Integer pidKey : pidKeys) { + TimeSeries series = map.get(pidKey); + + Number value = series.getValue(msec); + if (value != null) { + // found a match. lets check against the actual value. + if (value.doubleValue() == rangeValue) { + + return; + } + } + } + } + } + } + + + /** + * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable). + * Subclasses can override if necessary. + * <p/> + * This does nothing if the <code>Table</code> object is <code>null</code> (because the display + * type does not use a column) or if the <code>index</code>-th column is in fact the originating + * column passed as argument. + * + * @param index the index of the column to resize + * @param sourceColumn the original column that was resize, and on which we need to sync the + * index-th column width. + */ + void resizeColumn(int index, TableColumn sourceColumn) { + } + + /** + * Sets the current {@link EventLogParser} object. + * Subclasses can override if necessary. + */ + protected void setNewLogParser(EventLogParser logParser) { + } + + /** + * Prepares the {@link EventDisplay} for a multi event display. + */ + void startMultiEventDisplay() { + if (mLogTable != null) { + mLogTable.setRedraw(false); + } + } + + /** + * Finalizes the {@link EventDisplay} after a multi event display. + */ + void endMultiEventDisplay() { + if (mLogTable != null) { + mLogTable.setRedraw(true); + } + } + + /** + * Returns the {@link Table} object used to display events, if any. + * + * @return a Table object or <code>null</code>. + */ + Table getTable() { + return mLogTable; + } + + /** + * Loads a new {@link EventDisplay} from a storage string. The string must have been created + * with {@link #getStorageString()}. + * + * @param storageString the storage string + * @return a new {@link EventDisplay} or null if the load failed. + */ + static EventDisplay load(String storageString) { + if (storageString.length() > 0) { + // the storage string is separated by ':' + String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR)); + + try { + int index = 0; + + String name = values[index++]; + int displayType = Integer.parseInt(values[index++]); + boolean pidFiltering = Boolean.parseBoolean(values[index++]); + + EventDisplay ed = eventDisplayFactory(displayType, name); + ed.setPidFiltering(pidFiltering); + + // because empty sections are removed by String.split(), we have to check + // the index for those. + if (index < values.length) { + ed.loadPidFilters(values[index++]); + } + + if (index < values.length) { + ed.loadValueDescriptors(values[index++]); + } + + if (index < values.length) { + ed.loadOccurrenceDescriptors(values[index++]); + } + + ed.updateValueDescriptorCheck(); + + if (index < values.length) { + ed.mMaximumChartItemAge = Long.parseLong(values[index++]); + } + + if (index < values.length) { + ed.mHistWidth = Long.parseLong(values[index++]); + } + + return ed; + } catch (RuntimeException re) { + // we'll return null below. + Log.e("ddms", re); + } + } + + return null; + } + + private String getPidStorageString() { + if (mPidFilterList != null) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Integer i : mPidFilterList) { + if (first == false) { + sb.append(PID_STORAGE_SEPARATOR); + } else { + first = false; + } + sb.append(i); + } + + return sb.toString(); + } + return ""; //$NON-NLS-1$ + } + + + private void loadPidFilters(String storageString) { + if (storageString.length() > 0) { + String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR)); + + for (String value : values) { + if (mPidFilterList == null) { + mPidFilterList = new ArrayList<Integer>(); + } + mPidFilterList.add(Integer.parseInt(value)); + } + } + } + + private String getDescriptorStorageString( + ArrayList<? extends OccurrenceDisplayDescriptor> descriptorList) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + + for (OccurrenceDisplayDescriptor descriptor : descriptorList) { + if (first == false) { + sb.append(DESCRIPTOR_STORAGE_SEPARATOR); + } else { + first = false; + } + sb.append(descriptor.getStorageString()); + } + + return sb.toString(); + } + + private void loadOccurrenceDescriptors(String storageString) { + if (storageString.length() == 0) { + return; + } + + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); + + for (String value : values) { + OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor(); + desc.loadFrom(value); + mOccurrenceDescriptors.add(desc); + } + } + + private void loadValueDescriptors(String storageString) { + if (storageString.length() == 0) { + return; + } + + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); + + for (String value : values) { + ValueDisplayDescriptor desc = new ValueDisplayDescriptor(); + desc.loadFrom(value); + mValueDescriptors.add(desc); + } + } + + /** + * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another + * list if they are configured to display the {@link EventContainer} + * + * @param event the event container + * @param fullList the list with all the descriptors. + * @param outList the list to fill. + */ + @SuppressWarnings("unchecked") + private void getDescriptors(EventContainer event, + ArrayList<? extends OccurrenceDisplayDescriptor> fullList, + ArrayList outList) { + for (OccurrenceDisplayDescriptor descriptor : fullList) { + try { + // first check the event tag. + if (descriptor.eventTag == event.mTag) { + // now check if we have a filter on a value + if (descriptor.filterValueIndex == -1 || + event.testValue(descriptor.filterValueIndex, descriptor.filterValue, + descriptor.filterCompareMethod)) { + outList.add(descriptor); + } + } + } catch (InvalidTypeException ite) { + // if the filter for the descriptor was incorrect, we ignore the descriptor. + } catch (ArrayIndexOutOfBoundsException aioobe) { + // if the index was wrong (the event content may have changed since we setup the + // display), we do nothing but log the error + Log.e("Event Log", String.format( + "ArrayIndexOutOfBoundsException occured when checking %1$d-th value of event %2$d", //$NON-NLS-1$ + descriptor.filterValueIndex, descriptor.eventTag)); + } + } + } + + /** + * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor} + * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event. + * + * @param event + * @param valueDescriptors + * @param occurrenceDescriptors + * @return true if the event should be displayed. + */ + + protected boolean filterEvent(EventContainer event, + ArrayList<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) { + + // test the pid first (if needed) + if (mPidFiltering && mPidFilterList != null) { + boolean found = false; + for (int pid : mPidFilterList) { + if (pid == event.pid) { + found = true; + break; + } + } + + if (found == false) { + return false; + } + } + + // now get the list of matching descriptors + getDescriptors(event, mValueDescriptors, valueDescriptors); + getDescriptors(event, mOccurrenceDescriptors, occurrenceDescriptors); + + // and return whether there is at least one match in either list. + return (valueDescriptors.size() > 0 || occurrenceDescriptors.size() > 0); + } + + /** + * Checks all the {@link ValueDisplayDescriptor} for similarity. + * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG. + * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE + * + * @return flag as described above + */ + private int checkDescriptors() { + if (mValueDescriptors.size() < 2) { + return EVENT_CHECK_SAME_VALUE; + } + + int tag = -1; + int index = -1; + for (ValueDisplayDescriptor display : mValueDescriptors) { + if (tag == -1) { + tag = display.eventTag; + index = display.valueIndex; + } else { + if (tag != display.eventTag) { + return EVENT_CHECK_FAILED; + } else { + if (index != -1) { + if (index != display.valueIndex) { + index = -1; + } + } + } + } + } + + if (index == -1) { + return EVENT_CHECK_SAME_TAG; + } + + return EVENT_CHECK_SAME_VALUE; + } + + /** + * Resets the time limit on the chart to be infinite. + */ + void resetChartTimeLimit() { + mMaximumChartItemAge = -1; + } + + /** + * Sets the time limit on the charts. + * + * @param timeLimit the time limit in seconds. + */ + void setChartTimeLimit(long timeLimit) { + mMaximumChartItemAge = timeLimit; + } + + long getChartTimeLimit() { + return mMaximumChartItemAge; + } + + /** + * m + * Resets the histogram width + */ + void resetHistWidth() { + mHistWidth = 1; + } + + /** + * Sets the histogram width + * + * @param histWidth the width in hours + */ + void setHistWidth(long histWidth) { + mHistWidth = histWidth; + } + + long getHistWidth() { + return mHistWidth; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java new file mode 100644 index 0000000..88c3cb2 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java @@ -0,0 +1,955 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; +import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.List; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; + +class EventDisplayOptions extends Dialog { + private static final int DLG_WIDTH = 700; + private static final int DLG_HEIGHT = 700; + + private IImageLoader mImageLoader; + + private Shell mParent; + private Shell mShell; + + private boolean mEditStatus = false; + private final ArrayList<EventDisplay> mDisplayList = new ArrayList<EventDisplay>(); + + /* LEFT LIST */ + private List mEventDisplayList; + private Button mEventDisplayNewButton; + private Button mEventDisplayDeleteButton; + private Button mEventDisplayUpButton; + private Button mEventDisplayDownButton; + private Text mDisplayWidthText; + private Text mDisplayHeightText; + + /* WIDGETS ON THE RIGHT */ + private Text mDisplayNameText; + private Combo mDisplayTypeCombo; + private Group mChartOptions; + private Group mHistOptions; + private Button mPidFilterCheckBox; + private Text mPidText; + + /** Map with (event-tag, event name) */ + private Map<Integer, String> mEventTagMap; + + /** Map with (event-tag, array of value info for the event) */ + private Map<Integer, EventValueDescription[]> mEventDescriptionMap; + + /** list of current pids */ + private ArrayList<Integer> mPidList; + + private EventLogParser mLogParser; + + private Group mInfoGroup; + + private static class SelectionWidgets { + private List mList; + private Button mNewButton; + private Button mEditButton; + private Button mDeleteButton; + + private void setEnabled(boolean enable) { + mList.setEnabled(enable); + mNewButton.setEnabled(enable); + mEditButton.setEnabled(enable); + mDeleteButton.setEnabled(enable); + } + } + + private SelectionWidgets mValueSelection; + private SelectionWidgets mOccurrenceSelection; + + /** flag to temporarly disable processing of {@link Text} changes, so that + * {@link Text#setText(String)} can be called safely. */ + private boolean mProcessTextChanges = true; + private Text mTimeLimitText; + private Text mHistWidthText; + + EventDisplayOptions(IImageLoader imageLoader, Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageLoader = imageLoader; + } + + /** + * Opens the display option dialog, to edit the {@link EventDisplay} objects provided in the + * list. + * @param logParser + * @param displayList + * @param eventList + * @return true if the list of {@link EventDisplay} objects was updated. + */ + boolean open(EventLogParser logParser, ArrayList<EventDisplay> displayList, + ArrayList<EventContainer> eventList) { + mLogParser = logParser; + + if (logParser != null) { + // we need 2 things from the parser. + // the event tag / event name map + mEventTagMap = logParser.getTagMap(); + + // the event info map + mEventDescriptionMap = logParser.getEventInfoMap(); + } + + // make a copy of the EventDisplay list since we'll use working copies. + duplicateEventDisplay(displayList); + + // build a list of pid from the list of events. + buildPidList(eventList); + + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + // Set the dialog size. + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.layout(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mEditStatus; + } + + ArrayList<EventDisplay> getEventDisplays() { + return mDisplayList; + } + + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Event Display Configuration"); + + mShell.setLayout(new GridLayout(1, true)); + + final Composite topPanel = new Composite(mShell, SWT.NONE); + topPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + topPanel.setLayout(new GridLayout(2, false)); + + // create the tree on the left and the controls on the right. + Composite leftPanel = new Composite(topPanel, SWT.NONE); + Composite rightPanel = new Composite(topPanel, SWT.NONE); + + createLeftPanel(leftPanel); + createRightPanel(rightPanel); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + event.doit = true; + } + }); + + Label separator = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Composite bottomButtons = new Composite(mShell, SWT.NONE); + bottomButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + GridLayout gl; + bottomButtons.setLayout(gl = new GridLayout(2, true)); + gl.marginHeight = gl.marginWidth = 0; + + Button okButton = new Button(bottomButtons, SWT.PUSH); + okButton.setText("OK"); + okButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + Button cancelButton = new Button(bottomButtons, SWT.PUSH); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + // cancel the modification flag. + mEditStatus = false; + + // and close + mShell.close(); + } + }); + + enable(false); + + // fill the list with the current display + fillEventDisplayList(); + } + + private void createLeftPanel(Composite leftPanel) { + final IPreferenceStore store = DdmUiPreferences.getStore(); + + GridLayout gl; + + leftPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + leftPanel.setLayout(gl = new GridLayout(1, false)); + gl.verticalSpacing = 1; + + mEventDisplayList = new List(leftPanel, + SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.FULL_SELECTION); + mEventDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH)); + mEventDisplayList.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + handleEventDisplaySelection(); + } + }); + + Composite bottomControls = new Composite(leftPanel, SWT.NONE); + bottomControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + bottomControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + gl.verticalSpacing = 0; + gl.horizontalSpacing = 0; + + mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayNewButton.setImage(mImageLoader.loadImage("add.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayNewButton.setToolTipText("Adds a new event display"); + mEventDisplayNewButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + mEventDisplayNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + createNewEventDisplay(); + } + }); + + mEventDisplayDeleteButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayDeleteButton.setImage(mImageLoader.loadImage("delete.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayDeleteButton.setToolTipText("Deletes the selected event display"); + mEventDisplayDeleteButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + mEventDisplayDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + deleteEventDisplay(); + } + }); + + mEventDisplayUpButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayUpButton.setImage(mImageLoader.loadImage("up.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayUpButton.setToolTipText("Moves the selected event display up"); + mEventDisplayUpButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get current selection. + int selection = mEventDisplayList.getSelectionIndex(); + if (selection > 0) { + // update the list of EventDisplay. + EventDisplay display = mDisplayList.remove(selection); + mDisplayList.add(selection - 1, display); + + // update the list widget + mEventDisplayList.remove(selection); + mEventDisplayList.add(display.getName(), selection - 1); + + // update the selection and reset the ui. + mEventDisplayList.select(selection - 1); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + } + }); + + mEventDisplayDownButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayDownButton.setImage(mImageLoader.loadImage("down.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayDownButton.setToolTipText("Moves the selected event display down"); + mEventDisplayDownButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get current selection. + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1 && selection < mEventDisplayList.getItemCount() - 1) { + // update the list of EventDisplay. + EventDisplay display = mDisplayList.remove(selection); + mDisplayList.add(selection + 1, display); + + // update the list widget + mEventDisplayList.remove(selection); + mEventDisplayList.add(display.getName(), selection + 1); + + // update the selection and reset the ui. + mEventDisplayList.select(selection + 1); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + } + }); + + Group sizeGroup = new Group(leftPanel, SWT.NONE); + sizeGroup.setText("Display Size:"); + sizeGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + sizeGroup.setLayout(new GridLayout(2, false)); + + Label l = new Label(sizeGroup, SWT.NONE); + l.setText("Width:"); + + mDisplayWidthText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER); + mDisplayWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayWidthText.setText(Integer.toString( + store.getInt(EventLogPanel.PREFS_DISPLAY_WIDTH))); + mDisplayWidthText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + String text = mDisplayWidthText.getText().trim(); + try { + store.setValue(EventLogPanel.PREFS_DISPLAY_WIDTH, Integer.parseInt(text)); + setModified(); + } catch (NumberFormatException nfe) { + // do something? + } + } + }); + + l = new Label(sizeGroup, SWT.NONE); + l.setText("Height:"); + + mDisplayHeightText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER); + mDisplayHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayHeightText.setText(Integer.toString( + store.getInt(EventLogPanel.PREFS_DISPLAY_HEIGHT))); + mDisplayHeightText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + String text = mDisplayHeightText.getText().trim(); + try { + store.setValue(EventLogPanel.PREFS_DISPLAY_HEIGHT, Integer.parseInt(text)); + setModified(); + } catch (NumberFormatException nfe) { + // do something? + } + } + }); + } + + private void createRightPanel(Composite rightPanel) { + rightPanel.setLayout(new GridLayout(1, true)); + rightPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mInfoGroup = new Group(rightPanel, SWT.NONE); + mInfoGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mInfoGroup.setLayout(new GridLayout(2, false)); + + Label nameLabel = new Label(mInfoGroup, SWT.LEFT); + nameLabel.setText("Name:"); + + mDisplayNameText = new Text(mInfoGroup, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + mDisplayNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayNameText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mProcessTextChanges) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + eventDisplay.setName(mDisplayNameText.getText()); + int index = mEventDisplayList.getSelectionIndex(); + mEventDisplayList.remove(index); + mEventDisplayList.add(eventDisplay.getName(), index); + mEventDisplayList.select(index); + handleEventDisplaySelection(); + setModified(); + } + } + } + }); + + Label displayLabel = new Label(mInfoGroup, SWT.LEFT); + displayLabel.setText("Type:"); + + mDisplayTypeCombo = new Combo(mInfoGroup, SWT.READ_ONLY | SWT.DROP_DOWN); + mDisplayTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + // add the combo values. This must match the values EventDisplay.DISPLAY_TYPE_* + mDisplayTypeCombo.add("Log All"); + mDisplayTypeCombo.add("Filtered Log"); + mDisplayTypeCombo.add("Graph"); + mDisplayTypeCombo.add("Sync"); + mDisplayTypeCombo.add("Sync Histogram"); + mDisplayTypeCombo.add("Sync Performance"); + mDisplayTypeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null && eventDisplay.getDisplayType() != mDisplayTypeCombo.getSelectionIndex()) { + /* Replace the EventDisplay object with a different subclass */ + setModified(); + String name = eventDisplay.getName(); + EventDisplay newEventDisplay = EventDisplay.eventDisplayFactory(mDisplayTypeCombo.getSelectionIndex(), name); + setCurrentEventDisplay(newEventDisplay); + fillUiWith(newEventDisplay); + } + } + }); + + mChartOptions = new Group(mInfoGroup, SWT.NONE); + mChartOptions.setText("Chart Options"); + GridData gd; + mChartOptions.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + mChartOptions.setLayout(new GridLayout(2, false)); + + Label l = new Label(mChartOptions, SWT.NONE); + l.setText("Time Limit (seconds):"); + + mTimeLimitText = new Text(mChartOptions, SWT.BORDER); + mTimeLimitText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTimeLimitText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent arg0) { + String text = mTimeLimitText.getText().trim(); + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + try { + if (text.length() == 0) { + eventDisplay.resetChartTimeLimit(); + } else { + eventDisplay.setChartTimeLimit(Long.parseLong(text)); + } + } catch (NumberFormatException nfe) { + eventDisplay.resetChartTimeLimit(); + } finally { + setModified(); + } + } + } + }); + + mHistOptions = new Group(mInfoGroup, SWT.NONE); + mHistOptions.setText("Histogram Options"); + GridData gdh; + mHistOptions.setLayoutData(gdh = new GridData(GridData.FILL_HORIZONTAL)); + gdh.horizontalSpan = 2; + mHistOptions.setLayout(new GridLayout(2, false)); + + Label lh = new Label(mHistOptions, SWT.NONE); + lh.setText("Histogram width (hours):"); + + mHistWidthText = new Text(mHistOptions, SWT.BORDER); + mHistWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mHistWidthText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent arg0) { + String text = mHistWidthText.getText().trim(); + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + try { + if (text.length() == 0) { + eventDisplay.resetHistWidth(); + } else { + eventDisplay.setHistWidth(Long.parseLong(text)); + } + } catch (NumberFormatException nfe) { + eventDisplay.resetHistWidth(); + } finally { + setModified(); + } + } + } + }); + + mPidFilterCheckBox = new Button(mInfoGroup, SWT.CHECK); + mPidFilterCheckBox.setText("Enable filtering by pid"); + mPidFilterCheckBox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + mPidFilterCheckBox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + eventDisplay.setPidFiltering(mPidFilterCheckBox.getSelection()); + mPidText.setEnabled(mPidFilterCheckBox.getSelection()); + setModified(); + } + } + }); + + Label pidLabel = new Label(mInfoGroup, SWT.NONE); + pidLabel.setText("Pid Filter:"); + pidLabel.setToolTipText("Enter all pids, separated by commas"); + + mPidText = new Text(mInfoGroup, SWT.BORDER); + mPidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mPidText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mProcessTextChanges) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null && eventDisplay.getPidFiltering()) { + String pidText = mPidText.getText().trim(); + String[] pids = pidText.split("\\s*,\\s*"); //$NON-NLS-1$ + + ArrayList<Integer> list = new ArrayList<Integer>(); + for (String pid : pids) { + try { + list.add(Integer.valueOf(pid)); + } catch (NumberFormatException nfe) { + // just ignore non valid pid + } + } + + eventDisplay.setPidFilterList(list); + setModified(); + } + } + } + }); + + /* ------------------ + * EVENT VALUE/OCCURRENCE SELECTION + * ------------------ */ + mValueSelection = createEventSelection(rightPanel, ValueDisplayDescriptor.class, + "Event Value Display"); + mOccurrenceSelection = createEventSelection(rightPanel, OccurrenceDisplayDescriptor.class, + "Event Occurrence Display"); + } + + private SelectionWidgets createEventSelection(Composite rightPanel, + final Class<? extends OccurrenceDisplayDescriptor> descriptorClass, + String groupMessage) { + + Group eventSelectionPanel = new Group(rightPanel, SWT.NONE); + eventSelectionPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl; + eventSelectionPanel.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + eventSelectionPanel.setText(groupMessage); + + final SelectionWidgets widgets = new SelectionWidgets(); + + widgets.mList = new List(eventSelectionPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL); + widgets.mList.setLayoutData(new GridData(GridData.FILL_BOTH)); + widgets.mList.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + widgets.mDeleteButton.setEnabled(true); + widgets.mEditButton.setEnabled(true); + } else { + widgets.mDeleteButton.setEnabled(false); + widgets.mEditButton.setEnabled(false); + } + } + }); + + Composite rightControls = new Composite(eventSelectionPanel, SWT.NONE); + rightControls.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + rightControls.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + gl.verticalSpacing = 0; + gl.horizontalSpacing = 0; + + widgets.mNewButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mNewButton.setText("New..."); + widgets.mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mNewButton.setEnabled(false); + widgets.mNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + try { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + EventValueSelector dialog = new EventValueSelector(mShell); + if (dialog.open(descriptorClass, mLogParser)) { + eventDisplay.addDescriptor(dialog.getDescriptor()); + fillUiWith(eventDisplay); + setModified(); + } + } + } catch (Exception e1) { + e1.printStackTrace(); + } + } + }); + + widgets.mEditButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mEditButton.setText("Edit..."); + widgets.mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mEditButton.setEnabled(false); + widgets.mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // get the current descriptor index + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + // get the descriptor itself + OccurrenceDisplayDescriptor descriptor = eventDisplay.getDescriptor( + descriptorClass, index); + + // open the edit dialog. + EventValueSelector dialog = new EventValueSelector(mShell); + if (dialog.open(descriptor, mLogParser)) { + descriptor.replaceWith(dialog.getDescriptor()); + eventDisplay.updateValueDescriptorCheck(); + fillUiWith(eventDisplay); + + // reselect the item since fillUiWith remove the selection. + widgets.mList.select(index); + widgets.mList.notifyListeners(SWT.Selection, null); + + setModified(); + } + } + } + } + }); + + widgets.mDeleteButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mDeleteButton.setText("Delete"); + widgets.mDeleteButton.setEnabled(false); + widgets.mDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // get the current descriptor index + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + eventDisplay.removeDescriptor(descriptorClass, index); + fillUiWith(eventDisplay); + setModified(); + } + } + } + }); + + return widgets; + } + + + private void duplicateEventDisplay(ArrayList<EventDisplay> displayList) { + for (EventDisplay eventDisplay : displayList) { + mDisplayList.add(EventDisplay.clone(eventDisplay)); + } + } + + private void buildPidList(ArrayList<EventContainer> eventList) { + mPidList = new ArrayList<Integer>(); + for (EventContainer event : eventList) { + if (mPidList.indexOf(event.pid) == -1) { + mPidList.add(event.pid); + } + } + } + + private void setModified() { + mEditStatus = true; + } + + + private void enable(boolean status) { + mEventDisplayDeleteButton.setEnabled(status); + + // enable up/down + int selection = mEventDisplayList.getSelectionIndex(); + int count = mEventDisplayList.getItemCount(); + mEventDisplayUpButton.setEnabled(status && selection > 0); + mEventDisplayDownButton.setEnabled(status && selection != -1 && selection < count - 1); + + mDisplayNameText.setEnabled(status); + mDisplayTypeCombo.setEnabled(status); + mPidFilterCheckBox.setEnabled(status); + + mValueSelection.setEnabled(status); + mOccurrenceSelection.setEnabled(status); + mValueSelection.mNewButton.setEnabled(status); + mOccurrenceSelection.mNewButton.setEnabled(status); + if (status == false) { + mPidText.setEnabled(false); + } + } + + private void fillEventDisplayList() { + for (EventDisplay eventDisplay : mDisplayList) { + mEventDisplayList.add(eventDisplay.getName()); + } + } + + private void createNewEventDisplay() { + int count = mDisplayList.size(); + + String name = String.format("display %1$d", count + 1); + + EventDisplay eventDisplay = EventDisplay.eventDisplayFactory(0 /* type*/, name); + + mDisplayList.add(eventDisplay); + mEventDisplayList.add(name); + + mEventDisplayList.select(count); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + + private void deleteEventDisplay() { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + mDisplayList.remove(selection); + mEventDisplayList.remove(selection); + if (mDisplayList.size() < selection) { + selection--; + } + mEventDisplayList.select(selection); + handleEventDisplaySelection(); + + setModified(); + } + } + + private EventDisplay getCurrentEventDisplay() { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + return mDisplayList.get(selection); + } + + return null; + } + + private void setCurrentEventDisplay(EventDisplay eventDisplay) { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + mDisplayList.set(selection, eventDisplay); + } + } + + private void handleEventDisplaySelection() { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // enable the UI + enable(true); + + // and fill it + fillUiWith(eventDisplay); + } else { + // disable the UI + enable(false); + + // and empty it. + emptyUi(); + } + } + + private void emptyUi() { + mDisplayNameText.setText(""); + mDisplayTypeCombo.clearSelection(); + mValueSelection.mList.removeAll(); + mOccurrenceSelection.mList.removeAll(); + } + + private void fillUiWith(EventDisplay eventDisplay) { + mProcessTextChanges = false; + + mDisplayNameText.setText(eventDisplay.getName()); + int displayMode = eventDisplay.getDisplayType(); + mDisplayTypeCombo.select(displayMode); + if (displayMode == EventDisplay.DISPLAY_TYPE_GRAPH) { + GridData gd = (GridData) mChartOptions.getLayoutData(); + gd.exclude = false; + mChartOptions.setVisible(!gd.exclude); + long limit = eventDisplay.getChartTimeLimit(); + if (limit != -1) { + mTimeLimitText.setText(Long.toString(limit)); + } else { + mTimeLimitText.setText(""); //$NON-NLS-1$ + } + } else { + GridData gd = (GridData) mChartOptions.getLayoutData(); + gd.exclude = true; + mChartOptions.setVisible(!gd.exclude); + mTimeLimitText.setText(""); //$NON-NLS-1$ + } + + if (displayMode == EventDisplay.DISPLAY_TYPE_SYNC_HIST) { + GridData gd = (GridData) mHistOptions.getLayoutData(); + gd.exclude = false; + mHistOptions.setVisible(!gd.exclude); + long limit = eventDisplay.getHistWidth(); + if (limit != -1) { + mHistWidthText.setText(Long.toString(limit)); + } else { + mHistWidthText.setText(""); //$NON-NLS-1$ + } + } else { + GridData gd = (GridData) mHistOptions.getLayoutData(); + gd.exclude = true; + mHistOptions.setVisible(!gd.exclude); + mHistWidthText.setText(""); //$NON-NLS-1$ + } + mInfoGroup.layout(true); + mShell.layout(true); + mShell.pack(); + + if (eventDisplay.getPidFiltering()) { + mPidFilterCheckBox.setSelection(true); + mPidText.setEnabled(true); + + // build the pid list. + ArrayList<Integer> list = eventDisplay.getPidFilterList(); + if (list != null) { + StringBuilder sb = new StringBuilder(); + int count = list.size(); + for (int i = 0 ; i < count ; i++) { + sb.append(list.get(i)); + if (i < count - 1) { + sb.append(", ");//$NON-NLS-1$ + } + } + mPidText.setText(sb.toString()); + } else { + mPidText.setText(""); //$NON-NLS-1$ + } + } else { + mPidFilterCheckBox.setSelection(false); + mPidText.setEnabled(false); + mPidText.setText(""); //$NON-NLS-1$ + } + + mProcessTextChanges = true; + + mValueSelection.mList.removeAll(); + mOccurrenceSelection.mList.removeAll(); + + if (eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_FILTERED_LOG || + eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_GRAPH) { + mOccurrenceSelection.setEnabled(true); + mValueSelection.setEnabled(true); + + Iterator<ValueDisplayDescriptor> valueIterator = eventDisplay.getValueDescriptors(); + + while (valueIterator.hasNext()) { + ValueDisplayDescriptor descriptor = valueIterator.next(); + mValueSelection.mList.add(String.format("%1$s: %2$s [%3$s]%4$s", + mEventTagMap.get(descriptor.eventTag), descriptor.valueName, + getSeriesLabelDescription(descriptor), getFilterDescription(descriptor))); + } + + Iterator<OccurrenceDisplayDescriptor> occurrenceIterator = + eventDisplay.getOccurrenceDescriptors(); + + while (occurrenceIterator.hasNext()) { + OccurrenceDisplayDescriptor descriptor = occurrenceIterator.next(); + + mOccurrenceSelection.mList.add(String.format("%1$s [%2$s]%3$s", + mEventTagMap.get(descriptor.eventTag), + getSeriesLabelDescription(descriptor), + getFilterDescription(descriptor))); + } + + mValueSelection.mList.notifyListeners(SWT.Selection, null); + mOccurrenceSelection.mList.notifyListeners(SWT.Selection, null); + } else { + mOccurrenceSelection.setEnabled(false); + mValueSelection.setEnabled(false); + } + + } + + /** + * Returns a String describing what is used as the series label + * @param descriptor the descriptor of the display. + */ + private String getSeriesLabelDescription(OccurrenceDisplayDescriptor descriptor) { + if (descriptor.seriesValueIndex != -1) { + if (descriptor.includePid) { + return String.format("%1$s + pid", + mEventDescriptionMap.get( + descriptor.eventTag)[descriptor.seriesValueIndex].getName()); + } else { + return mEventDescriptionMap.get(descriptor.eventTag)[descriptor.seriesValueIndex] + .getName(); + } + } + return "pid"; + } + + private String getFilterDescription(OccurrenceDisplayDescriptor descriptor) { + if (descriptor.filterValueIndex != -1) { + return String.format(" [%1$s %2$s %3$s]", + mEventDescriptionMap.get( + descriptor.eventTag)[descriptor.filterValueIndex].getName(), + descriptor.filterCompareMethod.testString(), + descriptor.filterValue != null ? + descriptor.filterValue.toString() : "?"); //$NON-NLS-1$ + } + return ""; //$NON-NLS-1$ + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java new file mode 100644 index 0000000..a1303f6 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.Log; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +/** + * Imports a textual event log. Gets tags from build path. + */ +public class EventLogImporter { + + private String[] mTags; + private String[] mLog; + + public EventLogImporter(String filePath) throws FileNotFoundException { + String top = System.getenv("ANDROID_BUILD_TOP"); + if (top == null) { + throw new FileNotFoundException(); + } + final String tagFile = top + "/system/core/logcat/event-log-tags"; + BufferedReader tagReader = new BufferedReader( + new InputStreamReader(new FileInputStream(tagFile))); + BufferedReader eventReader = new BufferedReader( + new InputStreamReader(new FileInputStream(filePath))); + try { + readTags(tagReader); + readLog(eventReader); + } catch (IOException e) { + } + } + + public String[] getTags() { + return mTags; + } + + public String[] getLog() { + return mLog; + } + + private void readTags(BufferedReader reader) throws IOException { + String line; + + ArrayList<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + content.add(line); + } + mTags = content.toArray(new String[content.size()]); + } + + private void readLog(BufferedReader reader) throws IOException { + String line; + + ArrayList<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + content.add(line); + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java new file mode 100644 index 0000000..2621c6a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java @@ -0,0 +1,926 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.Client; +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.LogReceiver; +import com.android.ddmlib.log.LogReceiver.ILogListener; +import com.android.ddmlib.log.LogReceiver.LogEntry; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.TablePanel; +import com.android.ddmuilib.actions.ICommonAction; +import com.android.ddmuilib.annotation.UiThread; +import com.android.ddmuilib.annotation.WorkerThread; +import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.RowData; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.regex.Pattern; + +/** + * Event log viewer + */ +public class EventLogPanel extends TablePanel implements ILogListener, + ILogColumnListener { + + private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$ + + private final static String PREFS_EVENT_DISPLAY = "EventLogPanel.eventDisplay"; //$NON-NLS-1$ + private final static String EVENT_DISPLAY_STORAGE_SEPARATOR = "|"; //$NON-NLS-1$ + + static final String PREFS_DISPLAY_WIDTH = "EventLogPanel.width"; //$NON-NLS-1$ + static final String PREFS_DISPLAY_HEIGHT = "EventLogPanel.height"; //$NON-NLS-1$ + + private final static int DEFAULT_DISPLAY_WIDTH = 500; + private final static int DEFAULT_DISPLAY_HEIGHT = 400; + + private IImageLoader mImageLoader; + + private Device mCurrentLoggedDevice; + private String mCurrentLogFile; + private LogReceiver mCurrentLogReceiver; + private EventLogParser mCurrentEventLogParser; + + private Object mLock = new Object(); + + /** list of all the events. */ + private final ArrayList<EventContainer> mEvents = new ArrayList<EventContainer>(); + + /** list of all the new events, that have yet to be displayed by the ui */ + private final ArrayList<EventContainer> mNewEvents = new ArrayList<EventContainer>(); + /** indicates a pending ui thread display */ + private boolean mPendingDisplay = false; + + /** list of all the custom event displays */ + private final ArrayList<EventDisplay> mEventDisplays = new ArrayList<EventDisplay>(); + + private final NumberFormat mFormatter = NumberFormat.getInstance(); + private Composite mParent; + private ScrolledComposite mBottomParentPanel; + private Composite mBottomPanel; + private ICommonAction mOptionsAction; + private ICommonAction mClearAction; + private ICommonAction mSaveAction; + private ICommonAction mLoadAction; + private ICommonAction mImportAction; + + /** file containing the current log raw data. */ + private File mTempFile = null; + + public EventLogPanel(IImageLoader imageLoader) { + super(); + mImageLoader = imageLoader; + mFormatter.setGroupingUsed(true); + } + + /** + * Sets the external actions. + * <p/>This method sets up the {@link ICommonAction} objects to execute the proper code + * when triggered by using {@link ICommonAction#setRunnable(Runnable)}. + * <p/>It will also make sure they are enabled only when possible. + * @param optionsAction + * @param clearAction + * @param saveAction + * @param loadAction + * @param importAction + */ + public void setActions(ICommonAction optionsAction, ICommonAction clearAction, + ICommonAction saveAction, ICommonAction loadAction, ICommonAction importAction) { + mOptionsAction = optionsAction; + mOptionsAction.setRunnable(new Runnable() { + public void run() { + openOptionPanel(); + } + }); + + mClearAction = clearAction; + mClearAction.setRunnable(new Runnable() { + public void run() { + clearLog(); + } + }); + + mSaveAction = saveAction; + mSaveAction.setRunnable(new Runnable() { + public void run() { + try { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); + + fileDialog.setText("Save Event Log"); + fileDialog.setFileName("event.log"); + + String fileName = fileDialog.open(); + if (fileName != null) { + saveLog(fileName); + } + } catch (IOException e1) { + } + } + }); + + mLoadAction = loadAction; + mLoadAction.setRunnable(new Runnable() { + public void run() { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Load Event Log"); + + String fileName = fileDialog.open(); + if (fileName != null) { + loadLog(fileName); + } + } + }); + + mImportAction = importAction; + mImportAction.setRunnable(new Runnable() { + public void run() { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Import Bug Report"); + + String fileName = fileDialog.open(); + if (fileName != null) { + importBugReport(fileName); + } + } + }); + + mOptionsAction.setEnabled(false); + mClearAction.setEnabled(false); + mSaveAction.setEnabled(false); + } + + /** + * Opens the option panel. + * </p> + * <b>This must be called from the UI thread</b> + */ + @UiThread + public void openOptionPanel() { + try { + EventDisplayOptions dialog = new EventDisplayOptions(mImageLoader, mParent.getShell()); + if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) { + synchronized (mLock) { + // get the new EventDisplay list + mEventDisplays.clear(); + mEventDisplays.addAll(dialog.getEventDisplays()); + + // since the list of EventDisplay changed, we store it. + saveEventDisplays(); + + rebuildUi(); + } + } + } catch (SWTException e) { + Log.e("EventLog", e); //$NON-NLS-1$ + } + } + + /** + * Clears the log. + * <p/> + * <b>This must be called from the UI thread</b> + */ + public void clearLog() { + try { + synchronized (mLock) { + mEvents.clear(); + mNewEvents.clear(); + mPendingDisplay = false; + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.resetUI(); + } + } + } catch (SWTException e) { + Log.e("EventLog", e); //$NON-NLS-1$ + } + } + + /** + * Saves the content of the event log into a file. The log is saved in the same + * binary format than on the device. + * @param filePath + * @throws IOException + */ + public void saveLog(String filePath) throws IOException { + if (mCurrentLoggedDevice != null && mCurrentEventLogParser != null) { + File destFile = new File(filePath); + destFile.createNewFile(); + FileInputStream fis = new FileInputStream(mTempFile); + FileOutputStream fos = new FileOutputStream(destFile); + byte[] buffer = new byte[1024]; + + int count; + + while ((count = fis.read(buffer)) != -1) { + fos.write(buffer, 0, count); + } + + fos.close(); + fis.close(); + + // now we save the tag file + filePath = filePath + TAG_FILE_EXT; + mCurrentEventLogParser.saveTags(filePath); + } + } + + /** + * Loads a binary event log (if has associated .tag file) or + * otherwise loads a textual event log. + * @param filePath Event log path (and base of potential tag file) + */ + public void loadLog(String filePath) { + if ((new File(filePath + TAG_FILE_EXT)).exists()) { + startEventLogFromFiles(filePath); + } else { + try { + EventLogImporter importer = new EventLogImporter(filePath); + String[] tags = importer.getTags(); + String[] log = importer.getLog(); + startEventLogFromContent(tags, log); + } catch (FileNotFoundException e) { + // If this fails, display the error message from startEventLogFromFiles, + // and pretend we never tried EventLogImporter + Log.logAndDisplay(Log.LogLevel.ERROR, "EventLog", + String.format("Failure to read %1$s", filePath + TAG_FILE_EXT)); + } + + } + } + + public void importBugReport(String filePath) { + try { + BugReportImporter importer = new BugReportImporter(filePath); + + String[] tags = importer.getTags(); + String[] log = importer.getLog(); + + startEventLogFromContent(tags, log); + + } catch (FileNotFoundException e) { + Log.logAndDisplay(LogLevel.ERROR, "Import", + "Unable to import bug report: " + e.getMessage()); + } + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.SelectionDependentPanel#clientSelected() + */ + @Override + public void clientSelected() { + // pass + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.SelectionDependentPanel#deviceSelected() + */ + @Override + public void deviceSelected() { + startEventLog(getCurrentDevice()); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int) + */ + public void clientChanged(Client client, int changeMask) { + // pass + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.Panel#createControl(org.eclipse.swt.widgets.Composite) + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + mParent.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + synchronized (mLock) { + if (mCurrentLogReceiver != null) { + mCurrentLogReceiver.cancel(); + mCurrentLogReceiver = null; + mCurrentEventLogParser = null; + mCurrentLoggedDevice = null; + mEventDisplays.clear(); + mEvents.clear(); + } + } + } + }); + + final IPreferenceStore store = DdmUiPreferences.getStore(); + + // init some store stuff + store.setDefault(PREFS_DISPLAY_WIDTH, DEFAULT_DISPLAY_WIDTH); + store.setDefault(PREFS_DISPLAY_HEIGHT, DEFAULT_DISPLAY_HEIGHT); + + mBottomParentPanel = new ScrolledComposite(parent, SWT.V_SCROLL); + mBottomParentPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + mBottomParentPanel.setExpandHorizontal(true); + mBottomParentPanel.setExpandVertical(true); + + mBottomParentPanel.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + if (mBottomPanel != null) { + Rectangle r = mBottomParentPanel.getClientArea(); + mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width, + SWT.DEFAULT)); + } + } + }); + + prepareDisplayUi(); + + // load the EventDisplay from storage. + loadEventDisplays(); + + // create the ui + createDisplayUi(); + + return mBottomParentPanel; + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.Panel#postCreation() + */ + @Override + protected void postCreation() { + // pass + } + + /* (non-Javadoc) + * @see com.android.ddmuilib.Panel#setFocus() + */ + @Override + public void setFocus() { + mBottomParentPanel.setFocus(); + } + + /** + * Starts a new logcat and set mCurrentLogCat as the current receiver. + * @param device the device to connect logcat to. + */ + private void startEventLog(final Device device) { + if (device == mCurrentLoggedDevice) { + return; + } + + // if we have a logcat already running + if (mCurrentLogReceiver != null) { + stopEventLog(false); + } + mCurrentLoggedDevice = null; + mCurrentLogFile = null; + + if (device != null) { + // create a new output receiver + mCurrentLogReceiver = new LogReceiver(this); + + // start the logcat in a different thread + new Thread("EventLog") { //$NON-NLS-1$ + @Override + public void run() { + while (device.isOnline() == false && + mCurrentLogReceiver != null && + mCurrentLogReceiver.isCancelled() == false) { + try { + sleep(2000); + } catch (InterruptedException e) { + return; + } + } + + if (mCurrentLogReceiver == null || mCurrentLogReceiver.isCancelled()) { + // logcat was stopped/cancelled before the device became ready. + return; + } + + try { + mCurrentLoggedDevice = device; + synchronized (mLock) { + mCurrentEventLogParser = new EventLogParser(); + mCurrentEventLogParser.init(device); + } + + // update the event display with the new parser. + updateEventDisplays(); + + // prepare the temp file that will contain the raw data + mTempFile = File.createTempFile("android-event-", ".log"); + + device.runEventLogService(mCurrentLogReceiver); + } catch (Exception e) { + Log.e("EventLog", e); + } finally { + } + } + }.start(); + } + } + + private void startEventLogFromFiles(final String fileName) { + // if we have a logcat already running + if (mCurrentLogReceiver != null) { + stopEventLog(false); + } + mCurrentLoggedDevice = null; + mCurrentLogFile = null; + + // create a new output receiver + mCurrentLogReceiver = new LogReceiver(this); + + mSaveAction.setEnabled(false); + + // start the logcat in a different thread + new Thread("EventLog") { //$NON-NLS-1$ + @Override + public void run() { + try { + mCurrentLogFile = fileName; + synchronized (mLock) { + mCurrentEventLogParser = new EventLogParser(); + if (mCurrentEventLogParser.init(fileName + TAG_FILE_EXT) == false) { + mCurrentEventLogParser = null; + Log.logAndDisplay(LogLevel.ERROR, "EventLog", + String.format("Failure to read %1$s", fileName + TAG_FILE_EXT)); + return; + } + } + + // update the event display with the new parser. + updateEventDisplays(); + + runLocalEventLogService(fileName, mCurrentLogReceiver); + } catch (Exception e) { + Log.e("EventLog", e); + } finally { + } + } + }.start(); + } + + private void startEventLogFromContent(final String[] tags, final String[] log) { + // if we have a logcat already running + if (mCurrentLogReceiver != null) { + stopEventLog(false); + } + mCurrentLoggedDevice = null; + mCurrentLogFile = null; + + // create a new output receiver + mCurrentLogReceiver = new LogReceiver(this); + + mSaveAction.setEnabled(false); + + // start the logcat in a different thread + new Thread("EventLog") { //$NON-NLS-1$ + @Override + public void run() { + try { + synchronized (mLock) { + mCurrentEventLogParser = new EventLogParser(); + if (mCurrentEventLogParser.init(tags) == false) { + mCurrentEventLogParser = null; + return; + } + } + + // update the event display with the new parser. + updateEventDisplays(); + + runLocalEventLogService(log, mCurrentLogReceiver); + } catch (Exception e) { + Log.e("EventLog", e); + } finally { + } + } + }.start(); + } + + + public void stopEventLog(boolean inUiThread) { + if (mCurrentLogReceiver != null) { + mCurrentLogReceiver.cancel(); + + // when the thread finishes, no one will reference that object + // and it'll be destroyed + synchronized (mLock) { + mCurrentLogReceiver = null; + mCurrentEventLogParser = null; + + mCurrentLoggedDevice = null; + mEvents.clear(); + mNewEvents.clear(); + mPendingDisplay = false; + } + + resetUI(inUiThread); + } + + if (mTempFile != null) { + mTempFile.delete(); + mTempFile = null; + } + } + + private void resetUI(boolean inUiThread) { + mEvents.clear(); + + // the ui is static we just empty it. + if (inUiThread) { + resetUiFromUiThread(); + } else { + try { + Display d = mBottomParentPanel.getDisplay(); + + // run sync as we need to update right now. + d.syncExec(new Runnable() { + public void run() { + if (mBottomParentPanel.isDisposed() == false) { + resetUiFromUiThread(); + } + } + }); + } catch (SWTException e) { + // display is disposed, we're quitting. Do nothing. + } + } + } + + private void resetUiFromUiThread() { + synchronized(mLock) { + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.resetUI(); + } + } + mOptionsAction.setEnabled(false); + mClearAction.setEnabled(false); + mSaveAction.setEnabled(false); + } + + private void prepareDisplayUi() { + mBottomPanel = new Composite(mBottomParentPanel, SWT.NONE); + mBottomParentPanel.setContent(mBottomPanel); + } + + private void createDisplayUi() { + RowLayout rowLayout = new RowLayout(); + rowLayout.wrap = true; + rowLayout.pack = false; + rowLayout.justify = true; + rowLayout.fill = true; + rowLayout.type = SWT.HORIZONTAL; + mBottomPanel.setLayout(rowLayout); + + IPreferenceStore store = DdmUiPreferences.getStore(); + int displayWidth = store.getInt(PREFS_DISPLAY_WIDTH); + int displayHeight = store.getInt(PREFS_DISPLAY_HEIGHT); + + for (EventDisplay eventDisplay : mEventDisplays) { + Control c = eventDisplay.createComposite(mBottomPanel, mCurrentEventLogParser, this); + if (c != null) { + RowData rd = new RowData(); + rd.height = displayHeight; + rd.width = displayWidth; + c.setLayoutData(rd); + } + + Table table = eventDisplay.getTable(); + if (table != null) { + addTableToFocusListener(table); + } + } + + mBottomPanel.layout(); + mBottomParentPanel.setMinSize(mBottomPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + mBottomParentPanel.layout(); + } + + /** + * Rebuild the display ui. + */ + @UiThread + private void rebuildUi() { + synchronized (mLock) { + // we need to rebuild the ui. First we get rid of it. + mBottomPanel.dispose(); + mBottomPanel = null; + + prepareDisplayUi(); + createDisplayUi(); + + // and fill it + + boolean start_event = false; + synchronized (mNewEvents) { + mNewEvents.addAll(0, mEvents); + + if (mPendingDisplay == false) { + mPendingDisplay = true; + start_event = true; + } + } + + if (start_event) { + scheduleUIEventHandler(); + } + + Rectangle r = mBottomParentPanel.getClientArea(); + mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width, + SWT.DEFAULT)); + } + } + + + /** + * Processes a new {@link LogEntry} by parsing it with {@link EventLogParser} and displaying it. + * @param entry The new log entry + * @see LogReceiver.ILogListener#newEntry(LogEntry) + */ + @WorkerThread + public void newEntry(LogEntry entry) { + synchronized (mLock) { + if (mCurrentEventLogParser != null) { + EventContainer event = mCurrentEventLogParser.parse(entry); + if (event != null) { + handleNewEvent(event); + } + } + } + } + + @WorkerThread + private void handleNewEvent(EventContainer event) { + // add the event to the generic list + mEvents.add(event); + + // add to the list of events that needs to be displayed, and trigger a + // new display if needed. + boolean start_event = false; + synchronized (mNewEvents) { + mNewEvents.add(event); + + if (mPendingDisplay == false) { + mPendingDisplay = true; + start_event = true; + } + } + + if (start_event == false) { + // we're done + return; + } + + scheduleUIEventHandler(); + } + + /** + * Schedules the UI thread to execute a {@link Runnable} calling {@link #displayNewEvents()}. + */ + private void scheduleUIEventHandler() { + try { + Display d = mBottomParentPanel.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + if (mBottomParentPanel.isDisposed() == false) { + if (mCurrentEventLogParser != null) { + displayNewEvents(); + } + } + } + }); + } catch (SWTException e) { + // if the ui is disposed, do nothing + } + } + + /** + * Processes raw data coming from the log service. + * @see LogReceiver.ILogListener#newData(byte[], int, int) + */ + public void newData(byte[] data, int offset, int length) { + if (mTempFile != null) { + try { + FileOutputStream fos = new FileOutputStream(mTempFile, true /* append */); + fos.write(data, offset, length); + fos.close(); + } catch (FileNotFoundException e) { + } catch (IOException e) { + } + } + } + + @UiThread + private void displayNewEvents() { + // never display more than 1,000 events in this loop. We can't do too much in the UI thread. + int count = 0; + + // prepare the displays + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.startMultiEventDisplay(); + } + + // display the new events + EventContainer event = null; + boolean need_to_reloop = false; + do { + // get the next event to display. + synchronized (mNewEvents) { + if (mNewEvents.size() > 0) { + if (count > 200) { + // there are still events to be displayed, but we don't want to hog the + // UI thread for too long, so we stop this runnable, but launch a new + // one to keep going. + need_to_reloop = true; + event = null; + } else { + event = mNewEvents.remove(0); + count++; + } + } else { + // we're done. + event = null; + mPendingDisplay = false; + } + } + + if (event != null) { + // notify the event display + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.newEvent(event, mCurrentEventLogParser); + } + } + } while (event != null); + + // we're done displaying events. + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.endMultiEventDisplay(); + } + + // if needed, ask the UI thread to re-run this method. + if (need_to_reloop) { + scheduleUIEventHandler(); + } + } + + /** + * Loads the {@link EventDisplay}s from the preference store. + */ + private void loadEventDisplays() { + IPreferenceStore store = DdmUiPreferences.getStore(); + String storage = store.getString(PREFS_EVENT_DISPLAY); + + if (storage.length() > 0) { + String[] values = storage.split(Pattern.quote(EVENT_DISPLAY_STORAGE_SEPARATOR)); + + for (String value : values) { + EventDisplay eventDisplay = EventDisplay.load(value); + if (eventDisplay != null) { + mEventDisplays.add(eventDisplay); + } + } + } + } + + /** + * Saves the {@link EventDisplay}s into the {@link DdmUiPreferences} store. + */ + private void saveEventDisplays() { + IPreferenceStore store = DdmUiPreferences.getStore(); + + boolean first = true; + StringBuilder sb = new StringBuilder(); + + for (EventDisplay eventDisplay : mEventDisplays) { + String storage = eventDisplay.getStorageString(); + if (storage != null) { + if (first == false) { + sb.append(EVENT_DISPLAY_STORAGE_SEPARATOR); + } else { + first = false; + } + + sb.append(storage); + } + } + + store.setValue(PREFS_EVENT_DISPLAY, sb.toString()); + } + + /** + * Updates the {@link EventDisplay} with the new {@link EventLogParser}. + * <p/> + * This will run asynchronously in the UI thread. + */ + @WorkerThread + private void updateEventDisplays() { + try { + Display d = mBottomParentPanel.getDisplay(); + + d.asyncExec(new Runnable() { + public void run() { + if (mBottomParentPanel.isDisposed() == false) { + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.setNewLogParser(mCurrentEventLogParser); + } + + mOptionsAction.setEnabled(true); + mClearAction.setEnabled(true); + if (mCurrentLogFile == null) { + mSaveAction.setEnabled(true); + } else { + mSaveAction.setEnabled(false); + } + } + } + }); + } catch (SWTException e) { + // display is disposed: do nothing. + } + } + + @UiThread + public void columnResized(int index, TableColumn sourceColumn) { + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.resizeColumn(index, sourceColumn); + } + } + + /** + * Runs an event log service out of a local file. + * @param fileName the full file name of the local file containing the event log. + * @param logReceiver the receiver that will handle the log + * @throws IOException + */ + @WorkerThread + private void runLocalEventLogService(String fileName, LogReceiver logReceiver) + throws IOException { + byte[] buffer = new byte[256]; + + FileInputStream fis = new FileInputStream(fileName); + + int count; + while ((count = fis.read(buffer)) != -1) { + logReceiver.parseNewData(buffer, 0, count); + } + } + + @WorkerThread + private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) { + synchronized (mLock) { + for (String line : log) { + EventContainer event = mCurrentEventLogParser.parse(line); + if (event != null) { + handleNewEvent(event); + } + } + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java new file mode 100644 index 0000000..dd32e2c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java @@ -0,0 +1,628 @@ +/* + * Copyright (C) 2008 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.log.event; + +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.EventContainer.CompareMethod; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; +import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; + +final class EventValueSelector extends Dialog { + private static final int DLG_WIDTH = 400; + private static final int DLG_HEIGHT = 300; + + private Shell mParent; + private Shell mShell; + private boolean mEditStatus; + private Combo mEventCombo; + private Combo mValueCombo; + private Combo mSeriesCombo; + private Button mDisplayPidCheckBox; + private Combo mFilterCombo; + private Combo mFilterMethodCombo; + private Text mFilterValue; + private Button mOkButton; + + private EventLogParser mLogParser; + private OccurrenceDisplayDescriptor mDescriptor; + + /** list of event integer in the order of the combo. */ + private Integer[] mEventTags; + + /** list of indices in the {@link EventValueDescription} array of the current event + * that are of type string. This lets us get back the {@link EventValueDescription} from the + * index in the Series {@link Combo}. + */ + private final ArrayList<Integer> mSeriesIndices = new ArrayList<Integer>(); + + public EventValueSelector(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + } + + /** + * Opens the display option dialog to edit a new descriptor. + * @param decriptorClass the class of the object to instantiate. Must extend + * {@link OccurrenceDisplayDescriptor} + * @param logParser + * @return true if the object is to be created, false if the creation was canceled. + */ + boolean open(Class<? extends OccurrenceDisplayDescriptor> descriptorClass, + EventLogParser logParser) { + try { + OccurrenceDisplayDescriptor descriptor = descriptorClass.newInstance(); + setModified(); + return open(descriptor, logParser); + } catch (InstantiationException e) { + return false; + } catch (IllegalAccessException e) { + return false; + } + } + + /** + * Opens the display option dialog, to edit a {@link OccurrenceDisplayDescriptor} object or + * a {@link ValueDisplayDescriptor} object. + * @param descriptor The descriptor to edit. + * @return true if the object was modified. + */ + boolean open(OccurrenceDisplayDescriptor descriptor, EventLogParser logParser) { + // make a copy of the descriptor as we'll use a working copy. + if (descriptor instanceof ValueDisplayDescriptor) { + mDescriptor = new ValueDisplayDescriptor((ValueDisplayDescriptor)descriptor); + } else if (descriptor instanceof OccurrenceDisplayDescriptor) { + mDescriptor = new OccurrenceDisplayDescriptor(descriptor); + } else { + return false; + } + + mLogParser = logParser; + + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + loadValueDescriptor(); + + checkValidity(); + + // Set the dialog size. + try { + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + } catch (Exception e) { + e.printStackTrace(); + } + + mShell.layout(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mEditStatus; + } + + OccurrenceDisplayDescriptor getDescriptor() { + return mDescriptor; + } + + private void createUI() { + GridData gd; + + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Event Display Configuration"); + + mShell.setLayout(new GridLayout(2, false)); + + Label l = new Label(mShell, SWT.NONE); + l.setText("Event:"); + + mEventCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mEventCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // the event tag / event name map + Map<Integer, String> eventTagMap = mLogParser.getTagMap(); + Map<Integer, EventValueDescription[]> eventInfoMap = mLogParser.getEventInfoMap(); + Set<Integer> keys = eventTagMap.keySet(); + ArrayList<Integer> list = new ArrayList<Integer>(); + for (Integer i : keys) { + if (eventInfoMap.get(i) != null) { + String eventName = eventTagMap.get(i); + mEventCombo.add(eventName); + + list.add(i); + } + } + mEventTags = list.toArray(new Integer[list.size()]); + + mEventCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleEventComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Value:"); + + mValueCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mValueCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mValueCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleValueComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Series Name:"); + + mSeriesCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mSeriesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSeriesCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleSeriesComboSelection(); + setModified(); + } + }); + + // empty comp + new Composite(mShell, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = gd.widthHint = 0; + + mDisplayPidCheckBox = new Button(mShell, SWT.CHECK); + mDisplayPidCheckBox.setText("Also Show pid"); + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mDescriptor.includePid = mDisplayPidCheckBox.getSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter By:"); + + mFilterCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mFilterCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleFilterComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter Method:"); + + mFilterMethodCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mFilterMethodCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (CompareMethod method : CompareMethod.values()) { + mFilterMethodCombo.add(method.toString()); + } + mFilterMethodCombo.select(0); + mFilterMethodCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleFilterMethodComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter Value:"); + + mFilterValue = new Text(mShell, SWT.BORDER | SWT.SINGLE); + mFilterValue.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterValue.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mDescriptor.filterValueIndex != -1) { + // get the current selection in the event combo + int index = mEventCombo.getSelectionIndex(); + + if (index != -1) { + // match it to an event + int eventTag = mEventTags[index]; + mDescriptor.eventTag = eventTag; + + // get the EventValueDescription for this tag + EventValueDescription valueDesc = mLogParser.getEventInfoMap() + .get(eventTag)[mDescriptor.filterValueIndex]; + + // let the EventValueDescription convert the String value into an object + // of the proper type. + mDescriptor.filterValue = valueDesc.getObjectFromString( + mFilterValue.getText().trim()); + setModified(); + } + } + } + }); + + // add a separator spanning the 2 columns + + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + l.setLayoutData(gd); + + // add a composite to hold the ok/cancel button, no matter what the columns size are. + Composite buttonComp = new Composite(mShell, SWT.NONE); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + buttonComp.setLayoutData(gd); + GridLayout gl; + buttonComp.setLayout(gl = new GridLayout(6, true)); + gl.marginHeight = gl.marginWidth = 0; + + Composite padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mOkButton = new Button(buttonComp, SWT.PUSH); + mOkButton.setText("OK"); + mOkButton.setLayoutData(new GridData(GridData.CENTER)); + mOkButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Button cancelButton = new Button(buttonComp, SWT.PUSH); + cancelButton.setText("Cancel"); + cancelButton.setLayoutData(new GridData(GridData.CENTER)); + cancelButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + // cancel the edit + mEditStatus = false; + mShell.close(); + } + }); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + event.doit = true; + } + }); + } + + private void setModified() { + mEditStatus = true; + } + + private void handleEventComboSelection() { + // get the current selection in the event combo + int index = mEventCombo.getSelectionIndex(); + + if (index != -1) { + // match it to an event + int eventTag = mEventTags[index]; + mDescriptor.eventTag = eventTag; + + // get the EventValueDescription for this tag + EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); + + // fill the combo for the values + mValueCombo.removeAll(); + if (values != null) { + if (mDescriptor instanceof ValueDisplayDescriptor) { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; + + mValueCombo.setEnabled(true); + for (EventValueDescription value : values) { + mValueCombo.add(value.toString()); + } + + if (valueDescriptor.valueIndex != -1) { + mValueCombo.select(valueDescriptor.valueIndex); + } else { + mValueCombo.clearSelection(); + } + } else { + mValueCombo.setEnabled(false); + } + + // fill the axis combo + mSeriesCombo.removeAll(); + mSeriesCombo.setEnabled(false); + mSeriesIndices.clear(); + int axisIndex = 0; + int selectionIndex = -1; + for (EventValueDescription value : values) { + if (value.getEventValueType() == EventValueType.STRING) { + mSeriesCombo.add(value.getName()); + mSeriesCombo.setEnabled(true); + mSeriesIndices.add(axisIndex); + + if (mDescriptor.seriesValueIndex != -1 && + mDescriptor.seriesValueIndex == axisIndex) { + selectionIndex = axisIndex; + } + } + axisIndex++; + } + + if (mSeriesCombo.isEnabled()) { + mSeriesCombo.add("default (pid)", 0 /* index */); + mSeriesIndices.add(0 /* index */, -1 /* value */); + + // +1 because we added another item at index 0 + mSeriesCombo.select(selectionIndex + 1); + + if (selectionIndex >= 0) { + mDisplayPidCheckBox.setSelection(mDescriptor.includePid); + mDisplayPidCheckBox.setEnabled(true); + } else { + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.setSelection(false); + } + } else { + mDisplayPidCheckBox.setSelection(false); + mDisplayPidCheckBox.setEnabled(false); + } + + // fill the filter combo + mFilterCombo.setEnabled(true); + mFilterCombo.removeAll(); + mFilterCombo.add("(no filter)"); + for (EventValueDescription value : values) { + mFilterCombo.add(value.toString()); + } + + // select the current filter + mFilterCombo.select(mDescriptor.filterValueIndex + 1); + mFilterMethodCombo.select(getFilterMethodIndex(mDescriptor.filterCompareMethod)); + + // fill the current filter value + if (mDescriptor.filterValueIndex != -1) { + EventValueDescription valueInfo = values[mDescriptor.filterValueIndex]; + if (valueInfo.checkForType(mDescriptor.filterValue)) { + mFilterValue.setText(mDescriptor.filterValue.toString()); + } else { + mFilterValue.setText(""); + } + } else { + mFilterValue.setText(""); + } + } else { + disableSubCombos(); + } + } else { + disableSubCombos(); + } + + checkValidity(); + } + + /** + * + */ + private void disableSubCombos() { + mValueCombo.removeAll(); + mValueCombo.clearSelection(); + mValueCombo.setEnabled(false); + + mSeriesCombo.removeAll(); + mSeriesCombo.clearSelection(); + mSeriesCombo.setEnabled(false); + + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.setSelection(false); + + mFilterCombo.removeAll(); + mFilterCombo.clearSelection(); + mFilterCombo.setEnabled(false); + + mFilterValue.setEnabled(false); + mFilterValue.setText(""); + mFilterMethodCombo.setEnabled(false); + } + + private void handleValueComboSelection() { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; + + // get the current selection in the value combo + int index = mValueCombo.getSelectionIndex(); + valueDescriptor.valueIndex = index; + + // for now set the built-in name + + // get the current selection in the event combo + int eventIndex = mEventCombo.getSelectionIndex(); + + // match it to an event + int eventTag = mEventTags[eventIndex]; + + // get the EventValueDescription for this tag + EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); + + valueDescriptor.valueName = values[index].getName(); + + checkValidity(); + } + + private void handleSeriesComboSelection() { + // get the current selection in the axis combo + int index = mSeriesCombo.getSelectionIndex(); + + // get the actual value index from the list. + int valueIndex = mSeriesIndices.get(index); + + mDescriptor.seriesValueIndex = valueIndex; + + if (index > 0) { + mDisplayPidCheckBox.setEnabled(true); + mDisplayPidCheckBox.setSelection(mDescriptor.includePid); + } else { + mDisplayPidCheckBox.setSelection(false); + mDisplayPidCheckBox.setEnabled(false); + } + } + + private void handleFilterComboSelection() { + // get the current selection in the axis combo + int index = mFilterCombo.getSelectionIndex(); + + // decrement index by 1 since the item 0 means + // no filter (index = -1), and the rest is offset by 1 + index--; + + mDescriptor.filterValueIndex = index; + + if (index != -1) { + mFilterValue.setEnabled(true); + mFilterMethodCombo.setEnabled(true); + if (mDescriptor.filterValue instanceof String) { + mFilterValue.setText((String)mDescriptor.filterValue); + } + } else { + mFilterValue.setText(""); + mFilterValue.setEnabled(false); + mFilterMethodCombo.setEnabled(false); + } + } + + private void handleFilterMethodComboSelection() { + // get the current selection in the axis combo + int index = mFilterMethodCombo.getSelectionIndex(); + CompareMethod method = CompareMethod.values()[index]; + + mDescriptor.filterCompareMethod = method; + } + + /** + * Returns the index of the filter method + * @param filterCompareMethod the {@link CompareMethod} enum. + */ + private int getFilterMethodIndex(CompareMethod filterCompareMethod) { + CompareMethod[] values = CompareMethod.values(); + for (int i = 0 ; i < values.length ; i++) { + if (values[i] == filterCompareMethod) { + return i; + } + } + return -1; + } + + + private void loadValueDescriptor() { + // get the index from the eventTag. + int eventIndex = 0; + int comboIndex = -1; + for (int i : mEventTags) { + if (i == mDescriptor.eventTag) { + comboIndex = eventIndex; + break; + } + eventIndex++; + } + + if (comboIndex == -1) { + mEventCombo.clearSelection(); + } else { + mEventCombo.select(comboIndex); + } + + // get the event from the descriptor + handleEventComboSelection(); + } + + private void checkValidity() { + mOkButton.setEnabled(mEventCombo.getSelectionIndex() != -1 && + (((mDescriptor instanceof ValueDisplayDescriptor) == false) || + mValueCombo.getSelectionIndex() != -1)); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java new file mode 100644 index 0000000..3af1447 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008 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.log.event; + +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.CrosshairState; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYItemRendererState; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.data.xy.XYDataset; +import org.jfree.ui.RectangleEdge; + +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; + +/** + * Custom renderer to render event occurrence. This rendered ignores the y value, and simply + * draws a line from min to max at the time of the item. + */ +public class OccurrenceRenderer extends XYLineAndShapeRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public void drawItem(Graphics2D g2, + XYItemRendererState state, + Rectangle2D dataArea, + PlotRenderingInfo info, + XYPlot plot, + ValueAxis domainAxis, + ValueAxis rangeAxis, + XYDataset dataset, + int series, + int item, + CrosshairState crosshairState, + int pass) { + TimeSeriesCollection timeDataSet = (TimeSeriesCollection)dataset; + + // get the x value for the series/item. + double x = timeDataSet.getX(series, item).doubleValue(); + + // get the min/max of the range axis + double yMin = rangeAxis.getLowerBound(); + double yMax = rangeAxis.getUpperBound(); + + RectangleEdge domainEdge = plot.getDomainAxisEdge(); + RectangleEdge rangeEdge = plot.getRangeAxisEdge(); + + // convert the coordinates to java2d. + double x2D = domainAxis.valueToJava2D(x, dataArea, domainEdge); + double yMin2D = rangeAxis.valueToJava2D(yMin, dataArea, rangeEdge); + double yMax2D = rangeAxis.valueToJava2D(yMax, dataArea, rangeEdge); + + // get the paint information for the series/item + Paint p = getItemPaint(series, item); + Stroke s = getItemStroke(series, item); + + Line2D line = null; + PlotOrientation orientation = plot.getOrientation(); + if (orientation == PlotOrientation.HORIZONTAL) { + line = new Line2D.Double(yMin2D, x2D, yMax2D, x2D); + } + else if (orientation == PlotOrientation.VERTICAL) { + line = new Line2D.Double(x2D, yMin2D, x2D, yMax2D); + } + g2.setPaint(p); + g2.setStroke(s); + g2.draw(line); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java new file mode 100644 index 0000000..108c097 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2009 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.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; + +import java.awt.Color; + +abstract public class SyncCommon extends EventDisplay { + + // State information while processing the event stream + private int mLastState; // 0 if event started, 1 if event stopped + private long mLastStartTime; // ms + private long mLastStopTime; //ms + private String mLastDetails; + private int mLastSyncSource; // poll, server, user, etc. + + // Some common variables for sync display. These define the sync backends + //and how they should be displayed. + protected static final int CALENDAR = 0; + protected static final int GMAIL = 1; + protected static final int FEEDS = 2; + protected static final int CONTACTS = 3; + protected static final int ERRORS = 4; + protected static final int NUM_AUTHS = (CONTACTS + 1); + protected static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", + "Errors"}; + protected static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, + Color.ORANGE, Color.RED}; + + // Values from data/etc/event-log-tags + final int EVENT_SYNC = 2720; + final int EVENT_TICKLE = 2742; + final int EVENT_SYNC_DETAILS = 2743; + + protected SyncCommon(String name) { + super(name); + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + mLastStartTime = 0; + mLastStopTime = 0; + mLastState = -1; + mLastSyncSource = -1; + mLastDetails = ""; + } + + /** + * Updates the display with a new event. This is the main entry point for + * each event. This method has the logic to tie together the start event, + * stop event, and details event into one graph item. The combined sync event + * is handed to the subclass via processSycnEvent. Note that the details + * can happen before or after the stop event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + try { + if (event.mTag == EVENT_SYNC) { + int state = Integer.parseInt(event.getValueAsString(1)); + if (state == 0) { // start + mLastStartTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + mLastState = 0; + mLastSyncSource = Integer.parseInt(event.getValueAsString(2)); + mLastDetails = ""; + } else if (state == 1) { // stop + if (mLastState == 0) { + mLastStopTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + if (mLastStartTime == 0) { + // Log starts with a stop event + mLastStartTime = mLastStopTime; + } + int auth = getAuth(event.getValueAsString(0)); + processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, + true, mLastSyncSource); + mLastState = 1; + } + } + } else if (event.mTag == EVENT_SYNC_DETAILS) { + mLastDetails = event.getValueAsString(3); + if (mLastState != 0) { // Not inside event + long updateTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + if (updateTime - mLastStopTime <= 250) { + // Got details within 250ms after event, so delete and re-insert + // Details later than 250ms (arbitrary) are discarded as probably + // unrelated. + int auth = getAuth(event.getValueAsString(0)); + processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, + false, mLastSyncSource); + } + } + } + } catch (InvalidTypeException e) { + } + } + + /** + * Callback hook for subclass to process a sync event. newEvent has the logic + * to combine start and stop events and passes a processed event to the + * subclass. + * + * @param event The sync event + * @param auth The sync authority + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource Poll, user, server, etc. + */ + abstract void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource); + + /** + * Converts authority name to auth number. + * + * @param authname "calendar", etc. + * @return number series number associated with the authority + */ + protected int getAuth(String authname) throws InvalidTypeException { + if ("calendar".equals(authname) || "cl".equals(authname)) { + return CALENDAR; + } else if ("contacts".equals(authname) || "cp".equals(authname)) { + return CONTACTS; + } else if ("subscribedfeeds".equals(authname)) { + return FEEDS; + } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) { + return GMAIL; + } else if ("gmail-live".equals(authname)) { + return GMAIL; + } else if ("unknown".equals(authname)) { + return -1; // Unknown tickles; discard + } else { + throw new InvalidTypeException("Unknown authname " + authname); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java new file mode 100644 index 0000000..c66fe48 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java @@ -0,0 +1,353 @@ +/* + * 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.logcat; + +import com.android.ddmuilib.IImageLoader; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Small dialog box to edit a static port number. + */ +public class EditFilterDialog extends Dialog { + + private static final int DLG_WIDTH = 400; + private static final int DLG_HEIGHT = 250; + + private Shell mParent; + + private Shell mShell; + + private boolean mOk = false; + + private IImageLoader mImageLoader; + + /** + * Filter being edited or created + */ + private LogFilter mFilter; + + private String mName; + private String mTag; + private String mPid; + + /** Log level as an index of the drop-down combo + * @see getLogLevel + * @see getComboIndex + */ + private int mLogLevel; + + private Button mOkButton; + + private Label mPidWarning; + + public EditFilterDialog(IImageLoader imageLoader, Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageLoader = imageLoader; + } + + public EditFilterDialog(IImageLoader imageLoader, Shell shell, + LogFilter filter) { + this(imageLoader, shell); + mFilter = filter; + } + + /** + * Opens the dialog. The method will return when the user closes the dialog + * somehow. + * + * @return true if ok was pressed, false if cancelled. + */ + public boolean open() { + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.open(); + + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + // we're quitting with OK. + // Lets update the filter if needed + if (mOk) { + // if it was a "Create filter" action we need to create it first. + if (mFilter == null) { + mFilter = new LogFilter(mName); + } + + // setup the filter + mFilter.setTagMode(mTag); + + if (mPid != null && mPid.length() > 0) { + mFilter.setPidMode(Integer.parseInt(mPid)); + } else { + mFilter.setPidMode(-1); + } + + mFilter.setLogLevel(getLogLevel(mLogLevel)); + } + + return mOk; + } + + public LogFilter getFilter() { + return mFilter; + } + + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Log Filter"); + + mShell.setLayout(new GridLayout(1, false)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + } + }); + + // top part with the filter name + Composite nameComposite = new Composite(mShell, SWT.NONE); + nameComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + nameComposite.setLayout(new GridLayout(2, false)); + + Label l = new Label(nameComposite, SWT.NONE); + l.setText("Filter Name:"); + + final Text filterNameText = new Text(nameComposite, + SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + mName = mFilter.getName(); + if (mName != null) { + filterNameText.setText(mName); + } + } + filterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + filterNameText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mName = filterNameText.getText().trim(); + validate(); + } + }); + + // separator + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + + // center part with the filter parameters + Composite main = new Composite(mShell, SWT.NONE); + main.setLayoutData(new GridData(GridData.FILL_BOTH)); + main.setLayout(new GridLayout(3, false)); + + l = new Label(main, SWT.NONE); + l.setText("by Log Tag:"); + + final Text tagText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + mTag = mFilter.getTagFilter(); + if (mTag != null) { + tagText.setText(mTag); + } + } + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + tagText.setLayoutData(gd); + tagText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mTag = tagText.getText().trim(); + validate(); + } + }); + + l = new Label(main, SWT.NONE); + l.setText("by pid:"); + + final Text pidText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + if (mFilter.getPidFilter() != -1) { + mPid = Integer.toString(mFilter.getPidFilter()); + } else { + mPid = ""; + } + pidText.setText(mPid); + } + pidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + pidText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mPid = pidText.getText().trim(); + validate(); + } + }); + + mPidWarning = new Label(main, SWT.NONE); + mPidWarning.setImage(mImageLoader.loadImage("empty.png", // $NON-NLS-1$ + mShell.getDisplay())); + + l = new Label(main, SWT.NONE); + l.setText("by Log level:"); + + final Combo logCombo = new Combo(main, SWT.DROP_DOWN | SWT.READ_ONLY); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + logCombo.setLayoutData(gd); + + // add the labels + logCombo.add("<none>"); + logCombo.add("Error"); + logCombo.add("Warning"); + logCombo.add("Info"); + logCombo.add("Debug"); + logCombo.add("Verbose"); + + if (mFilter != null) { + mLogLevel = getComboIndex(mFilter.getLogLevel()); + logCombo.select(mLogLevel); + } else { + logCombo.select(0); + } + + logCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection + mLogLevel = logCombo.getSelectionIndex(); + validate(); + } + }); + + // separator + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // bottom part with the ok/cancel + Composite bottomComp = new Composite(mShell, SWT.NONE); + bottomComp + .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + bottomComp.setLayout(new GridLayout(2, true)); + + mOkButton = new Button(bottomComp, SWT.NONE); + mOkButton.setText("OK"); + mOkButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mOk = true; + mShell.close(); + } + }); + mOkButton.setEnabled(false); + mShell.setDefaultButton(mOkButton); + + Button cancelButton = new Button(bottomComp, SWT.NONE); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + validate(); + } + + /** + * Returns the log level from a combo index. + * @param index the Combo index + * @return a log level valid for the Log class. + */ + protected int getLogLevel(int index) { + if (index == 0) { + return -1; + } + + return 7 - index; + } + + /** + * Returns the index in the combo that matches the log level + * @param logLevel The Log level. + * @return the combo index + */ + private int getComboIndex(int logLevel) { + if (logLevel == -1) { + return 0; + } + + return 7 - logLevel; + } + + /** + * Validates the content of the 2 text fields and enable/disable "ok", while + * setting up the warning/error message. + */ + private void validate() { + + // then we check it only contains digits. + if (mPid != null) { + if (mPid.matches("[0-9]*") == false) { // $NON-NLS-1$ + mOkButton.setEnabled(false); + mPidWarning.setImage(mImageLoader.loadImage( + "warning.png", // $NON-NLS-1$ + mShell.getDisplay())); + return; + } else { + mPidWarning.setImage(mImageLoader.loadImage( + "empty.png", // $NON-NLS-1$ + mShell.getDisplay())); + } + } + + if (mName == null || mName.length() == 0) { + mOkButton.setEnabled(false); + return; + } + + mOkButton.setEnabled(true); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java new file mode 100644 index 0000000..9cff656 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java @@ -0,0 +1,27 @@ +/* + * 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.logcat; + +import org.eclipse.swt.graphics.Color; + +public class LogColors { + public Color infoColor; + public Color debugColor; + public Color errorColor; + public Color warningColor; + public Color verboseColor; +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java new file mode 100644 index 0000000..a32de2f --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java @@ -0,0 +1,555 @@ +/* + * 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.logcat; + +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.annotation.UiThread; +import com.android.ddmuilib.logcat.LogPanel.LogMessage; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.regex.PatternSyntaxException; + +/** logcat output filter class */ +public class LogFilter { + + public final static int MODE_PID = 0x01; + public final static int MODE_TAG = 0x02; + public final static int MODE_LEVEL = 0x04; + + private String mName; + + /** + * Filtering mode. Value can be a mix of MODE_PID, MODE_TAG, MODE_LEVEL + */ + private int mMode = 0; + + /** + * pid used for filtering. Only valid if mMode is MODE_PID. + */ + private int mPid; + + /** Single level log level as defined in Log.mLevelChar. Only valid + * if mMode is MODE_LEVEL */ + private int mLogLevel; + + /** + * log tag filtering. Only valid if mMode is MODE_TAG + */ + private String mTag; + + private Table mTable; + private TabItem mTabItem; + private boolean mIsCurrentTabItem = false; + private int mUnreadCount = 0; + + /** Temp keyword filtering */ + private String[] mTempKeywordFilters; + + /** temp pid filtering */ + private int mTempPid = -1; + + /** temp tag filtering */ + private String mTempTag; + + /** temp log level filtering */ + private int mTempLogLevel = -1; + + private LogColors mColors; + + private boolean mTempFilteringStatus = false; + + private final ArrayList<LogMessage> mMessages = new ArrayList<LogMessage>(); + private final ArrayList<LogMessage> mNewMessages = new ArrayList<LogMessage>(); + + private boolean mSupportsDelete = true; + private boolean mSupportsEdit = true; + private int mRemovedMessageCount = 0; + + /** + * Creates a filter with a particular mode. + * @param name The name to be displayed in the UI + */ + public LogFilter(String name) { + mName = name; + } + + public LogFilter() { + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(mName); + + sb.append(':'); + sb.append(mMode); + if ((mMode & MODE_PID) == MODE_PID) { + sb.append(':'); + sb.append(mPid); + } + + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + sb.append(':'); + sb.append(mLogLevel); + } + + if ((mMode & MODE_TAG) == MODE_TAG) { + sb.append(':'); + sb.append(mTag); + } + + return sb.toString(); + } + + public boolean loadFromString(String string) { + String[] segments = string.split(":"); // $NON-NLS-1$ + int index = 0; + + // get the name + mName = segments[index++]; + + // get the mode + mMode = Integer.parseInt(segments[index++]); + + if ((mMode & MODE_PID) == MODE_PID) { + mPid = Integer.parseInt(segments[index++]); + } + + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + mLogLevel = Integer.parseInt(segments[index++]); + } + + if ((mMode & MODE_TAG) == MODE_TAG) { + mTag = segments[index++]; + } + + return true; + } + + + /** Sets the name of the filter. */ + void setName(String name) { + mName = name; + } + + /** + * Returns the UI display name. + */ + public String getName() { + return mName; + } + + /** + * Set the Table ui widget associated with this filter. + * @param tabItem The item in the TabFolder + * @param table The Table object + */ + public void setWidgets(TabItem tabItem, Table table) { + mTable = table; + mTabItem = tabItem; + } + + /** + * Returns true if the filter is ready for ui. + */ + public boolean uiReady() { + return (mTable != null && mTabItem != null); + } + + /** + * Returns the UI table object. + * @return + */ + public Table getTable() { + return mTable; + } + + public void dispose() { + mTable.dispose(); + mTabItem.dispose(); + mTable = null; + mTabItem = null; + } + + /** + * Resets the filtering mode to be 0 (i.e. no filter). + */ + public void resetFilteringMode() { + mMode = 0; + } + + /** + * Returns the current filtering mode. + * @return A bitmask. Possible values are MODE_PID, MODE_TAG, MODE_LEVEL + */ + public int getFilteringMode() { + return mMode; + } + + /** + * Adds PID to the current filtering mode. + * @param pid + */ + public void setPidMode(int pid) { + if (pid != -1) { + mMode |= MODE_PID; + } else { + mMode &= ~MODE_PID; + } + mPid = pid; + } + + /** Returns the pid filter if valid, otherwise -1 */ + public int getPidFilter() { + if ((mMode & MODE_PID) == MODE_PID) + return mPid; + return -1; + } + + public void setTagMode(String tag) { + if (tag != null && tag.length() > 0) { + mMode |= MODE_TAG; + } else { + mMode &= ~MODE_TAG; + } + mTag = tag; + } + + public String getTagFilter() { + if ((mMode & MODE_TAG) == MODE_TAG) + return mTag; + return null; + } + + public void setLogLevel(int level) { + if (level == -1) { + mMode &= ~MODE_LEVEL; + } else { + mMode |= MODE_LEVEL; + mLogLevel = level; + } + + } + + public int getLogLevel() { + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + return mLogLevel; + } + + return -1; + } + + + public boolean supportsDelete() { + return mSupportsDelete ; + } + + public boolean supportsEdit() { + return mSupportsEdit; + } + + /** + * Sets the selected state of the filter. + * @param selected selection state. + */ + public void setSelectedState(boolean selected) { + if (selected) { + if (mTabItem != null) { + mTabItem.setText(mName); + } + mUnreadCount = 0; + } + mIsCurrentTabItem = selected; + } + + /** + * Adds a new message and optionally removes an old message. + * <p/>The new message is filtered through {@link #accept(LogMessage)}. + * Calls to {@link #flush()} from a UI thread will display it (and other + * pending messages) to the associated {@link Table}. + * @param logMessage the MessageData object to filter + * @return true if the message was accepted. + */ + public boolean addMessage(LogMessage newMessage, LogMessage oldMessage) { + synchronized (mMessages) { + if (oldMessage != null) { + int index = mMessages.indexOf(oldMessage); + if (index != -1) { + // TODO check that index will always be -1 or 0, as only the oldest message is ever removed. + mMessages.remove(index); + mRemovedMessageCount++; + } + + // now we look for it in mNewMessages. This can happen if the new message is added + // and then removed because too many messages are added between calls to #flush() + index = mNewMessages.indexOf(oldMessage); + if (index != -1) { + // TODO check that index will always be -1 or 0, as only the oldest message is ever removed. + mNewMessages.remove(index); + } + } + + boolean filter = accept(newMessage); + + if (filter) { + // at this point the message is accepted, we add it to the list + mMessages.add(newMessage); + mNewMessages.add(newMessage); + } + + return filter; + } + } + + /** + * Removes all the items in the filter and its {@link Table}. + */ + public void clear() { + mRemovedMessageCount = 0; + mNewMessages.clear(); + mMessages.clear(); + mTable.removeAll(); + } + + /** + * Filters a message. + * @param logMessage the Message + * @return true if the message is accepted by the filter. + */ + boolean accept(LogMessage logMessage) { + // do the regular filtering now + if ((mMode & MODE_PID) == MODE_PID && mPid != logMessage.data.pid) { + return false; + } + + if ((mMode & MODE_TAG) == MODE_TAG && ( + logMessage.data.tag == null || + logMessage.data.tag.equals(mTag) == false)) { + return false; + } + + int msgLogLevel = logMessage.data.logLevel.getPriority(); + + // test the temp log filtering first, as it replaces the old one + if (mTempLogLevel != -1) { + if (mTempLogLevel > msgLogLevel) { + return false; + } + } else if ((mMode & MODE_LEVEL) == MODE_LEVEL && + mLogLevel > msgLogLevel) { + return false; + } + + // do the temp filtering now. + if (mTempKeywordFilters != null) { + String msg = logMessage.msg; + + for (String kw : mTempKeywordFilters) { + try { + if (msg.contains(kw) == false && msg.matches(kw) == false) { + return false; + } + } catch (PatternSyntaxException e) { + // if the string is not a valid regular expression, + // this exception is thrown. + return false; + } + } + } + + if (mTempPid != -1 && mTempPid != logMessage.data.pid) { + return false; + } + + if (mTempTag != null && mTempTag.length() > 0) { + if (mTempTag.equals(logMessage.data.tag) == false) { + return false; + } + } + + return true; + } + + /** + * Takes all the accepted messages and display them. + * This must be called from a UI thread. + */ + @UiThread + public void flush() { + // if scroll bar is at the bottom, we will scroll + ScrollBar bar = mTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // if we are not going to scroll, get the current first item being shown. + int topIndex = mTable.getTopIndex(); + + // disable drawing + mTable.setRedraw(false); + + int totalCount = mNewMessages.size(); + + try { + // remove the items of the old messages. + for (int i = 0 ; i < mRemovedMessageCount && mTable.getItemCount() > 0 ; i++) { + mTable.remove(0); + } + + if (mUnreadCount > mTable.getItemCount()) { + mUnreadCount = mTable.getItemCount(); + } + + // add the new items + for (int i = 0 ; i < totalCount ; i++) { + LogMessage msg = mNewMessages.get(i); + addTableItem(msg); + } + } catch (SWTException e) { + // log the error and keep going. Content of the logcat table maybe unexpected + // but at least ddms won't crash. + Log.e("LogFilter", e); + } + + // redraw + mTable.setRedraw(true); + + // scroll if needed, by showing the last item + if (scroll) { + totalCount = mTable.getItemCount(); + if (totalCount > 0) { + mTable.showItem(mTable.getItem(totalCount-1)); + } + } else if (mRemovedMessageCount > 0) { + // we need to make sure the topIndex is still visible. + // Because really old items are removed from the list, this could make it disappear + // if we don't change the scroll value at all. + + topIndex -= mRemovedMessageCount; + if (topIndex < 0) { + // looks like it disappeared. Lets just show the first item + mTable.showItem(mTable.getItem(0)); + } else { + mTable.showItem(mTable.getItem(topIndex)); + } + } + + // if this filter is not the current one, we update the tab text + // with the amount of unread message + if (mIsCurrentTabItem == false) { + mUnreadCount += mNewMessages.size(); + totalCount = mTable.getItemCount(); + if (mUnreadCount > 0) { + mTabItem.setText(mName + " (" // $NON-NLS-1$ + + (mUnreadCount > totalCount ? totalCount : mUnreadCount) + + ")"); // $NON-NLS-1$ + } else { + mTabItem.setText(mName); // $NON-NLS-1$ + } + } + + mNewMessages.clear(); + } + + void setColors(LogColors colors) { + mColors = colors; + } + + int getUnreadCount() { + return mUnreadCount; + } + + void setUnreadCount(int unreadCount) { + mUnreadCount = unreadCount; + } + + void setSupportsDelete(boolean support) { + mSupportsDelete = support; + } + + void setSupportsEdit(boolean support) { + mSupportsEdit = support; + } + + void setTempKeywordFiltering(String[] segments) { + mTempKeywordFilters = segments; + mTempFilteringStatus = true; + } + + void setTempPidFiltering(int pid) { + mTempPid = pid; + mTempFilteringStatus = true; + } + + void setTempTagFiltering(String tag) { + mTempTag = tag; + mTempFilteringStatus = true; + } + + void resetTempFiltering() { + if (mTempPid != -1 || mTempTag != null || mTempKeywordFilters != null) { + mTempFilteringStatus = true; + } + + mTempPid = -1; + mTempTag = null; + mTempKeywordFilters = null; + } + + void resetTempFilteringStatus() { + mTempFilteringStatus = false; + } + + boolean getTempFilterStatus() { + return mTempFilteringStatus; + } + + + /** + * Add a TableItem for the index-th item of the buffer + * @param filter The index of the table in which to insert the item. + */ + private void addTableItem(LogMessage msg) { + TableItem item = new TableItem(mTable, SWT.NONE); + item.setText(0, msg.data.time); + item.setText(1, new String(new char[] { msg.data.logLevel.getPriorityLetter() })); + item.setText(2, msg.data.pidString); + item.setText(3, msg.data.tag); + item.setText(4, msg.msg); + + // add the buffer index as data + item.setData(msg); + + if (msg.data.logLevel == LogLevel.INFO) { + item.setForeground(mColors.infoColor); + } else if (msg.data.logLevel == LogLevel.DEBUG) { + item.setForeground(mColors.debugColor); + } else if (msg.data.logLevel == LogLevel.ERROR) { + item.setForeground(mColors.errorColor); + } else if (msg.data.logLevel == LogLevel.WARN) { + item.setForeground(mColors.warningColor); + } else { + item.setForeground(mColors.verboseColor); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java new file mode 100644 index 0000000..bd8b75c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java @@ -0,0 +1,1571 @@ +/* + * 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.logcat; + +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.SelectionDependentPanel; +import com.android.ddmuilib.TableHelper; +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; +import com.android.ddmuilib.actions.ICommonAction; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LogPanel extends SelectionDependentPanel { + + private static final int STRING_BUFFER_LENGTH = 10000; + + /** no filtering. Only one tab with everything. */ + public static final int FILTER_NONE = 0; + /** manual mode for filter. all filters are manually created. */ + public static final int FILTER_MANUAL = 1; + /** automatic mode for filter (pid mode). + * All filters are automatically created. */ + public static final int FILTER_AUTO_PID = 2; + /** automatic mode for filter (tag mode). + * All filters are automatically created. */ + public static final int FILTER_AUTO_TAG = 3; + /** Manual filtering mode + new filter for debug app, if needed */ + public static final int FILTER_DEBUG = 4; + + public static final int COLUMN_MODE_MANUAL = 0; + public static final int COLUMN_MODE_AUTO = 1; + + public static String PREFS_TIME; + public static String PREFS_LEVEL; + public static String PREFS_PID; + public static String PREFS_TAG; + public static String PREFS_MESSAGE; + + /** + * This pattern is meant to parse the first line of a log message with the option + * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the + * following lines are the message (can be several line).<br> + * This first line looks something like<br> + * <code>"[ 00-00 00:00:00.000 <pid>:0x<???> <severity>/<tag>]"</code> + * <br> + * Note: severity is one of V, D, I, W, or EM<br> + * Note: the fraction of second value can have any number of digit. + * Note the tag should be trim as it may have spaces at the end. + */ + private static Pattern sLogPattern = Pattern.compile( + "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + //$NON-NLS-1$ + "\\s+(\\d*):(0x[0-9a-fA-F]+)\\s([VDIWE])/(.*)\\]$"); //$NON-NLS-1$ + + /** + * Interface for Storage Filter manager. Implementation of this interface + * provide a custom way to archive an reload filters. + */ + public interface ILogFilterStorageManager { + + public LogFilter[] getFilterFromStore(); + + public void saveFilters(LogFilter[] filters); + + public boolean requiresDefaultFilter(); + } + + private Composite mParent; + private IPreferenceStore mStore; + + /** top object in the view */ + private TabFolder mFolders; + + private LogColors mColors; + + private ILogFilterStorageManager mFilterStorage; + + private LogCatOuputReceiver mCurrentLogCat; + + /** + * Circular buffer containing the logcat output. This is unfiltered. + * The valid content goes from <code>mBufferStart</code> to + * <code>mBufferEnd - 1</code>. Therefore its number of item is + * <code>mBufferEnd - mBufferStart</code>. + */ + private LogMessage[] mBuffer = new LogMessage[STRING_BUFFER_LENGTH]; + + /** Represents the oldest message in the buffer */ + private int mBufferStart = -1; + + /** + * Represents the next usable item in the buffer to receive new message. + * This can be equal to mBufferStart, but when used mBufferStart will be + * incremented as well. + */ + private int mBufferEnd = -1; + + /** Filter list */ + private LogFilter[] mFilters; + + /** Default filter */ + private LogFilter mDefaultFilter; + + /** Current filter being displayed */ + private LogFilter mCurrentFilter; + + /** Filtering mode */ + private int mFilterMode = FILTER_NONE; + + /** Device currently running logcat */ + private Device mCurrentLoggedDevice = null; + + private ICommonAction mDeleteFilterAction; + private ICommonAction mEditFilterAction; + + private ICommonAction[] mLogLevelActions; + + /** message data, separated from content for multi line messages */ + protected static class LogMessageInfo { + public LogLevel logLevel; + public int pid; + public String pidString; + public String tag; + public String time; + } + + /** pointer to the latest LogMessageInfo. this is used for multi line + * log message, to reuse the info regarding level, pid, etc... + */ + private LogMessageInfo mLastMessageInfo = null; + + private boolean mPendingAsyncRefresh = false; + + /** loader for the images. the implementation will varie between standalone + * app and eclipse plugin app and eclipse plugin. */ + private IImageLoader mImageLoader; + + private String mDefaultLogSave; + + private int mColumnMode = COLUMN_MODE_MANUAL; + private Font mDisplayFont; + + private ITableFocusListener mGlobalListener; + + /** message data, separated from content for multi line messages */ + protected static class LogMessage { + public LogMessageInfo data; + public String msg; + + @Override + public String toString() { + return data.time + ": " //$NON-NLS-1$ + + data.logLevel + "/" //$NON-NLS-1$ + + data.tag + "(" //$NON-NLS-1$ + + data.pidString + "): " //$NON-NLS-1$ + + msg; + } + } + + /** + * objects able to receive the output of a remote shell command, + * specifically a logcat command in this case + */ + private final class LogCatOuputReceiver extends MultiLineReceiver { + + public boolean isCancelled = false; + + public LogCatOuputReceiver() { + super(); + + setTrimLine(false); + } + + @Override + public void processNewLines(String[] lines) { + if (isCancelled == false) { + processLogLines(lines); + } + } + + public boolean isCancelled() { + return isCancelled; + } + } + + /** + * Parser class for the output of a "ps" shell command executed on a device. + * This class looks for a specific pid to find the process name from it. + * Once found, the name is used to update a filter and a tab object + * + */ + private class PsOutputReceiver extends MultiLineReceiver { + + private LogFilter mFilter; + + private TabItem mTabItem; + + private int mPid; + + /** set to true when we've found the pid we're looking for */ + private boolean mDone = false; + + PsOutputReceiver(int pid, LogFilter filter, TabItem tabItem) { + mPid = pid; + mFilter = filter; + mTabItem = tabItem; + } + + public boolean isCancelled() { + return mDone; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.startsWith("USER")) { //$NON-NLS-1$ + continue; + } + // get the pid. + int index = line.indexOf(' '); + if (index == -1) { + continue; + } + // look for the next non blank char + index++; + while (line.charAt(index) == ' ') { + index++; + } + + // this is the start of the pid. + // look for the end. + int index2 = line.indexOf(' ', index); + + // get the line + String pidStr = line.substring(index, index2); + int pid = Integer.parseInt(pidStr); + if (pid != mPid) { + continue; + } else { + // get the process name + index = line.lastIndexOf(' '); + final String name = line.substring(index + 1); + + mFilter.setName(name); + + // update the tab + Display d = mFolders.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + mTabItem.setText(name); + } + }); + + // we're done with this ps. + mDone = true; + return; + } + } + } + + } + + + /** + * Create the log view with some default parameters + * @param imageLoader the image loader. + * @param colors The display color object + * @param filterStorage the storage for user defined filters. + * @param mode The filtering mode + */ + public LogPanel(IImageLoader imageLoader, LogColors colors, + ILogFilterStorageManager filterStorage, int mode) { + mImageLoader = imageLoader; + mColors = colors; + mFilterMode = mode; + mFilterStorage = filterStorage; + mStore = DdmUiPreferences.getStore(); + } + + public void setActions(ICommonAction deleteAction, ICommonAction editAction, + ICommonAction[] logLevelActions) { + mDeleteFilterAction = deleteAction; + mEditFilterAction = editAction; + mLogLevelActions = logLevelActions; + } + + /** + * Sets the column mode. Must be called before creatUI + * @param mode the column mode. Valid values are COLUMN_MOD_MANUAL and + * COLUMN_MODE_AUTO + */ + public void setColumnMode(int mode) { + mColumnMode = mode; + } + + /** + * Sets the display font. + * @param font The display font. + */ + public void setFont(Font font) { + mDisplayFont = font; + + if (mFilters != null) { + for (LogFilter f : mFilters) { + Table table = f.getTable(); + if (table != null) { + table.setFont(font); + } + } + } + + if (mDefaultFilter != null) { + Table table = mDefaultFilter.getTable(); + if (table != null) { + table.setFont(font); + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + startLogCat(getCurrentDevice()); + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + // pass + } + + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + + Composite top = new Composite(parent, SWT.NONE); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + top.setLayout(new GridLayout(1, false)); + + // create the tab folder + mFolders = new TabFolder(top, SWT.NONE); + mFolders.setLayoutData(new GridData(GridData.FILL_BOTH)); + mFolders.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentFilter != null) { + mCurrentFilter.setSelectedState(false); + } + mCurrentFilter = getCurrentFilter(); + mCurrentFilter.setSelectedState(true); + updateColumns(mCurrentFilter.getTable()); + if (mCurrentFilter.getTempFilterStatus()) { + initFilter(mCurrentFilter); + } + selectionChanged(mCurrentFilter); + } + }); + + + Composite bottom = new Composite(top, SWT.NONE); + bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + bottom.setLayout(new GridLayout(3, false)); + + Label label = new Label(bottom, SWT.NONE); + label.setText("Filter:"); + + final Text filterText = new Text(bottom, SWT.SINGLE | SWT.BORDER); + filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + filterText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + updateFilteringWith(filterText.getText()); + } + }); + + /* + Button addFilterBtn = new Button(bottom, SWT.NONE); + addFilterBtn.setImage(mImageLoader.loadImage("add.png", //$NON-NLS-1$ + addFilterBtn.getDisplay())); + */ + + // get the filters + createFilters(); + + // for each filter, create a tab. + int index = 0; + + if (mDefaultFilter != null) { + createTab(mDefaultFilter, index++, false); + } + + if (mFilters != null) { + for (LogFilter f : mFilters) { + createTab(f, index++, false); + } + } + + return top; + } + + @Override + protected void postCreation() { + // pass + } + + /** + * Sets the focus to the proper object. + */ + @Override + public void setFocus() { + mFolders.setFocus(); + } + + + /** + * Starts a new logcat and set mCurrentLogCat as the current receiver. + * @param device the device to connect logcat to. + */ + public void startLogCat(final Device device) { + if (device == mCurrentLoggedDevice) { + return; + } + + // if we have a logcat already running + if (mCurrentLoggedDevice != null) { + stopLogCat(false); + mCurrentLoggedDevice = null; + } + + resetUI(false); + + if (device != null) { + // create a new output receiver + mCurrentLogCat = new LogCatOuputReceiver(); + + // start the logcat in a different thread + new Thread("Logcat") { //$NON-NLS-1$ + @Override + public void run() { + + while (device.isOnline() == false && + mCurrentLogCat != null && + mCurrentLogCat.isCancelled == false) { + try { + sleep(2000); + } catch (InterruptedException e) { + return; + } + } + + if (mCurrentLogCat == null || mCurrentLogCat.isCancelled) { + // logcat was stopped/cancelled before the device became ready. + return; + } + + try { + mCurrentLoggedDevice = device; + device.executeShellCommand("logcat -v long", mCurrentLogCat); //$NON-NLS-1$ + } catch (Exception e) { + Log.e("Logcat", e); + } finally { + // at this point the command is terminated. + mCurrentLogCat = null; + mCurrentLoggedDevice = null; + } + } + }.start(); + } + } + + /** Stop the current logcat */ + public void stopLogCat(boolean inUiThread) { + if (mCurrentLogCat != null) { + mCurrentLogCat.isCancelled = true; + + // when the thread finishes, no one will reference that object + // and it'll be destroyed + mCurrentLogCat = null; + + // reset the content buffer + for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) { + mBuffer[i] = null; + } + + // because it's a circular buffer, it's hard to know if + // the array is empty with both start/end at 0 or if it's full + // with both start/end at 0 as well. So to mean empty, we use -1 + mBufferStart = -1; + mBufferEnd = -1; + + resetFilters(); + resetUI(inUiThread); + } + } + + /** + * Adds a new Filter. This methods displays the UI to create the filter + * and set up its parameters.<br> + * <b>MUST</b> be called from the ui thread. + * + */ + public void addFilter() { + EditFilterDialog dlg = new EditFilterDialog(mImageLoader, + mFolders.getShell()); + if (dlg.open()) { + synchronized (mBuffer) { + // get the new filter in the array + LogFilter filter = dlg.getFilter(); + addFilterToArray(filter); + + int index = mFilters.length - 1; + if (mDefaultFilter != null) { + index++; + } + + if (false) { + + for (LogFilter f : mFilters) { + if (f.uiReady()) { + f.dispose(); + } + } + if (mDefaultFilter != null && mDefaultFilter.uiReady()) { + mDefaultFilter.dispose(); + } + + // for each filter, create a tab. + int i = 0; + if (mFilters != null) { + for (LogFilter f : mFilters) { + createTab(f, i++, true); + } + } + if (mDefaultFilter != null) { + createTab(mDefaultFilter, i++, true); + } + } else { + + // create ui for the filter. + createTab(filter, index, true); + + // reset the default as it shouldn't contain the content of + // this new filter. + if (mDefaultFilter != null) { + initDefaultFilter(); + } + } + + // select the new filter + if (mCurrentFilter != null) { + mCurrentFilter.setSelectedState(false); + } + mFolders.setSelection(index); + filter.setSelectedState(true); + mCurrentFilter = filter; + + selectionChanged(filter); + + // finally we update the filtering mode if needed + if (mFilterMode == FILTER_NONE) { + mFilterMode = FILTER_MANUAL; + } + + mFilterStorage.saveFilters(mFilters); + + } + } + } + + /** + * Edits the current filter. The method displays the UI to edit the filter. + */ + public void editFilter() { + if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) { + EditFilterDialog dlg = new EditFilterDialog(mImageLoader, + mFolders.getShell(), + mCurrentFilter); + if (dlg.open()) { + synchronized (mBuffer) { + // at this point the filter has been updated. + // so we update its content + initFilter(mCurrentFilter); + + // and the content of the "other" filter as well. + if (mDefaultFilter != null) { + initDefaultFilter(); + } + + mFilterStorage.saveFilters(mFilters); + } + } + } + } + + /** + * Deletes the current filter. + */ + public void deleteFilter() { + synchronized (mBuffer) { + if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) { + // remove the filter from the list + removeFilterFromArray(mCurrentFilter); + mCurrentFilter.dispose(); + + // select the new filter + mFolders.setSelection(0); + if (mFilters.length > 0) { + mCurrentFilter = mFilters[0]; + } else { + mCurrentFilter = mDefaultFilter; + } + + selectionChanged(mCurrentFilter); + + // update the content of the "other" filter to include what was filtered out + // by the deleted filter. + if (mDefaultFilter != null) { + initDefaultFilter(); + } + + mFilterStorage.saveFilters(mFilters); + } + } + } + + /** + * saves the current selection in a text file. + * @return false if the saving failed. + */ + public boolean save() { + synchronized (mBuffer) { + FileDialog dlg = new FileDialog(mParent.getShell(), SWT.SAVE); + String fileName; + + dlg.setText("Save log..."); + dlg.setFileName("log.txt"); + String defaultPath = mDefaultLogSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + dlg.setFilterPath(defaultPath); + dlg.setFilterNames(new String[] { + "Text Files (*.txt)" + }); + dlg.setFilterExtensions(new String[] { + "*.txt" + }); + + fileName = dlg.open(); + if (fileName != null) { + mDefaultLogSave = dlg.getFilterPath(); + + // get the current table and its selection + Table currentTable = mCurrentFilter.getTable(); + + int[] selection = currentTable.getSelectionIndices(); + + // we need to sort the items to be sure. + Arrays.sort(selection); + + // loop on the selection and output the file. + try { + FileWriter writer = new FileWriter(fileName); + + for (int i : selection) { + TableItem item = currentTable.getItem(i); + LogMessage msg = (LogMessage)item.getData(); + String line = msg.toString(); + writer.write(line); + writer.write('\n'); + } + writer.flush(); + + } catch (IOException e) { + return false; + } + } + } + + return true; + } + + /** + * Empty the current circular buffer. + */ + public void clear() { + synchronized (mBuffer) { + for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) { + mBuffer[i] = null; + } + + mBufferStart = -1; + mBufferEnd = -1; + + // now we clear the existing filters + for (LogFilter filter : mFilters) { + filter.clear(); + } + + // and the default one + if (mDefaultFilter != null) { + mDefaultFilter.clear(); + } + } + } + + /** + * Copies the current selection of the current filter as multiline text. + * + * @param clipboard The clipboard to place the copied content. + */ + public void copy(Clipboard clipboard) { + // get the current table and its selection + Table currentTable = mCurrentFilter.getTable(); + + copyTable(clipboard, currentTable); + } + + /** + * Selects all lines. + */ + public void selectAll() { + Table currentTable = mCurrentFilter.getTable(); + currentTable.selectAll(); + } + + /** + * Sets a TableFocusListener which will be notified when one of the tables + * gets or loses focus. + * + * @param listener + */ + public void setTableFocusListener(ITableFocusListener listener) { + // record the global listener, to make sure table created after + // this call will still be setup. + mGlobalListener = listener; + + // now we setup the existing filters + for (LogFilter filter : mFilters) { + Table table = filter.getTable(); + + addTableToFocusListener(table); + } + + // and the default one + if (mDefaultFilter != null) { + addTableToFocusListener(mDefaultFilter.getTable()); + } + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus. + * + * @param table the Table object. + */ + private void addTableToFocusListener(final Table table) { + // create the activator for this table + final IFocusedTableActivator activator = new IFocusedTableActivator() { + public void copy(Clipboard clipboard) { + copyTable(clipboard, table); + } + + public void selectAll() { + table.selectAll(); + } + }; + + // add the focus listener on the table to notify the global listener + table.addFocusListener(new FocusListener() { + public void focusGained(FocusEvent e) { + mGlobalListener.focusGained(activator); + } + + public void focusLost(FocusEvent e) { + mGlobalListener.focusLost(activator); + } + }); + } + + /** + * Copies the current selection of a Table into the provided Clipboard, as + * multi-line text. + * + * @param clipboard The clipboard to place the copied content. + * @param table The table to copy from. + */ + private static void copyTable(Clipboard clipboard, Table table) { + int[] selection = table.getSelectionIndices(); + + // we need to sort the items to be sure. + Arrays.sort(selection); + + // all lines must be concatenated. + StringBuilder sb = new StringBuilder(); + + // loop on the selection and output the file. + for (int i : selection) { + TableItem item = table.getItem(i); + LogMessage msg = (LogMessage)item.getData(); + String line = msg.toString(); + sb.append(line); + sb.append('\n'); + } + + // now add that to the clipboard + clipboard.setContents(new Object[] { + sb.toString() + }, new Transfer[] { + TextTransfer.getInstance() + }); + } + + /** + * Sets the log level for the current filter, but does not save it. + * @param i + */ + public void setCurrentFilterLogLevel(int i) { + LogFilter filter = getCurrentFilter(); + + filter.setLogLevel(i); + + initFilter(filter); + } + + /** + * Creates a new tab in the folderTab item. Must be called from the ui + * thread. + * @param filter The filter associated with the tab. + * @param index the index of the tab. if -1, the tab will be added at the + * end. + * @param fillTable If true the table is filled with the current content of + * the buffer. + * @return The TabItem object that was created. + */ + private TabItem createTab(LogFilter filter, int index, boolean fillTable) { + synchronized (mBuffer) { + TabItem item = null; + if (index != -1) { + item = new TabItem(mFolders, SWT.NONE, index); + } else { + item = new TabItem(mFolders, SWT.NONE); + } + item.setText(filter.getName()); + + // set the control (the parent is the TabFolder item, always) + Composite top = new Composite(mFolders, SWT.NONE); + item.setControl(top); + + top.setLayout(new FillLayout()); + + // create the ui, first the table + final Table t = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + + if (mDisplayFont != null) { + t.setFont(mDisplayFont); + } + + // give the ui objects to the filters. + filter.setWidgets(item, t); + + t.setHeaderVisible(true); + t.setLinesVisible(false); + + if (mGlobalListener != null) { + addTableToFocusListener(t); + } + + // create a controllistener that will handle the resizing of all the + // columns (except the last) and of the table itself. + ControlListener listener = null; + if (mColumnMode == COLUMN_MODE_AUTO) { + listener = new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + Rectangle r = t.getClientArea(); + + // get the size of all but the last column + int total = t.getColumn(0).getWidth(); + total += t.getColumn(1).getWidth(); + total += t.getColumn(2).getWidth(); + total += t.getColumn(3).getWidth(); + + if (r.width > total) { + t.getColumn(4).setWidth(r.width-total); + } + } + }; + + t.addControlListener(listener); + } + + // then its column + TableColumn col = TableHelper.createTableColumn(t, "Time", SWT.LEFT, + "00-00 00:00:00", //$NON-NLS-1$ + PREFS_TIME, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + col.addControlListener(listener); + } + + col = TableHelper.createTableColumn(t, "", SWT.CENTER, + "D", //$NON-NLS-1$ + PREFS_LEVEL, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + col.addControlListener(listener); + } + + col = TableHelper.createTableColumn(t, "pid", SWT.LEFT, + "9999", //$NON-NLS-1$ + PREFS_PID, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + col.addControlListener(listener); + } + + col = TableHelper.createTableColumn(t, "tag", SWT.LEFT, + "abcdefgh", //$NON-NLS-1$ + PREFS_TAG, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + col.addControlListener(listener); + } + + col = TableHelper.createTableColumn(t, "Message", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz0123456789", //$NON-NLS-1$ + PREFS_MESSAGE, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + // instead of listening on resize for the last column, we make + // it non resizable. + col.setResizable(false); + } + + if (fillTable) { + initFilter(filter); + } + return item; + } + } + + protected void updateColumns(Table table) { + if (table != null) { + int index = 0; + TableColumn col; + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_TIME)); + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_LEVEL)); + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_PID)); + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_TAG)); + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_MESSAGE)); + } + } + + public void resetUI(boolean inUiThread) { + if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) { + if (inUiThread) { + mFolders.dispose(); + mParent.pack(true); + createControl(mParent); + } else { + Display d = mFolders.getDisplay(); + + // run sync as we need to update right now. + d.syncExec(new Runnable() { + public void run() { + mFolders.dispose(); + mParent.pack(true); + createControl(mParent); + } + }); + } + } else { + // the ui is static we just empty it. + if (mFolders.isDisposed() == false) { + if (inUiThread) { + emptyTables(); + } else { + Display d = mFolders.getDisplay(); + + // run sync as we need to update right now. + d.syncExec(new Runnable() { + public void run() { + if (mFolders.isDisposed() == false) { + emptyTables(); + } + } + }); + } + } + } + } + + /** + * Process new Log lines coming from {@link LogCatOuputReceiver}. + * @param lines the new lines + */ + protected void processLogLines(String[] lines) { + // WARNING: this will not work if the string contains more line than + // the buffer holds. + + if (lines.length > STRING_BUFFER_LENGTH) { + Log.e("LogCat", "Receiving more lines than STRING_BUFFER_LENGTH"); + } + + // parse the lines and create LogMessage that are stored in a temporary list + final ArrayList<LogMessage> newMessages = new ArrayList<LogMessage>(); + + synchronized (mBuffer) { + for (String line : lines) { + // ignore empty lines. + if (line.length() > 0) { + // check for header lines. + Matcher matcher = sLogPattern.matcher(line); + if (matcher.matches()) { + // this is a header line, parse the header and keep it around. + mLastMessageInfo = new LogMessageInfo(); + + mLastMessageInfo.time = matcher.group(1); + mLastMessageInfo.pidString = matcher.group(2); + mLastMessageInfo.pid = Integer.valueOf(mLastMessageInfo.pidString); + mLastMessageInfo.logLevel = LogLevel.getByLetterString(matcher.group(4)); + mLastMessageInfo.tag = matcher.group(5).trim(); + } else { + // This is not a header line. + // Create a new LogMessage and process it. + LogMessage mc = new LogMessage(); + + if (mLastMessageInfo == null) { + // The first line of output wasn't preceded + // by a header line; make something up so + // that users of mc.data don't NPE. + mLastMessageInfo = new LogMessageInfo(); + mLastMessageInfo.time = "??-?? ??:??:??.???"; //$NON-NLS1$ + mLastMessageInfo.pidString = "<unknown>"; //$NON-NLS1$ + mLastMessageInfo.pid = 0; + mLastMessageInfo.logLevel = LogLevel.INFO; + mLastMessageInfo.tag = "<unknown>"; //$NON-NLS1$ + } + + // If someone printed a log message with + // embedded '\n' characters, there will + // one header line followed by multiple text lines. + // Use the last header that we saw. + mc.data = mLastMessageInfo; + + // tabs seem to display as only 1 tab so we replace the leading tabs + // by 4 spaces. + mc.msg = line.replaceAll("\t", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + // process the new LogMessage. + processNewMessage(mc); + + // store the new LogMessage + newMessages.add(mc); + } + } + } + + // if we don't have a pending Runnable that will do the refresh, we ask the Display + // to run one in the UI thread. + if (mPendingAsyncRefresh == false) { + mPendingAsyncRefresh = true; + + try { + Display display = mFolders.getDisplay(); + + // run in sync because this will update the buffer start/end indices + display.asyncExec(new Runnable() { + public void run() { + asyncRefresh(); + } + }); + } catch (SWTException e) { + // display is disposed, we're probably quitting. Let's stop. + stopLogCat(false); + } + } + } + } + + /** + * Refreshes the UI with new messages. + */ + private void asyncRefresh() { + if (mFolders.isDisposed() == false) { + synchronized (mBuffer) { + try { + // the circular buffer has been updated, let have the filter flush their + // display with the new messages. + if (mFilters != null) { + for (LogFilter f : mFilters) { + f.flush(); + } + } + + if (mDefaultFilter != null) { + mDefaultFilter.flush(); + } + } finally { + // the pending refresh is done. + mPendingAsyncRefresh = false; + } + } + } else { + stopLogCat(true); + } + } + + /** + * Processes a new Message. + * <p/>This adds the new message to the buffer, and gives it to the existing filters. + * @param newMessage + */ + private void processNewMessage(LogMessage newMessage) { + // if we are in auto filtering mode, make sure we have + // a filter for this + if (mFilterMode == FILTER_AUTO_PID || + mFilterMode == FILTER_AUTO_TAG) { + checkFilter(newMessage.data); + } + + // compute the index where the message goes. + // was the buffer empty? + int messageIndex = -1; + if (mBufferStart == -1) { + messageIndex = mBufferStart = 0; + mBufferEnd = 1; + } else { + messageIndex = mBufferEnd; + + // check we aren't overwriting start + if (mBufferEnd == mBufferStart) { + mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH; + } + + // increment the next usable slot index + mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH; + } + + LogMessage oldMessage = null; + + // record the message that was there before + if (mBuffer[messageIndex] != null) { + oldMessage = mBuffer[messageIndex]; + } + + // then add the new one + mBuffer[messageIndex] = newMessage; + + // give the new message to every filters. + boolean filtered = false; + if (mFilters != null) { + for (LogFilter f : mFilters) { + filtered |= f.addMessage(newMessage, oldMessage); + } + } + if (filtered == false && mDefaultFilter != null) { + mDefaultFilter.addMessage(newMessage, oldMessage); + } + } + + private void createFilters() { + if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) { + // unarchive the filters. + mFilters = mFilterStorage.getFilterFromStore(); + + // set the colors + if (mFilters != null) { + for (LogFilter f : mFilters) { + f.setColors(mColors); + } + } + + if (mFilterStorage.requiresDefaultFilter()) { + mDefaultFilter = new LogFilter("Log"); + mDefaultFilter.setColors(mColors); + mDefaultFilter.setSupportsDelete(false); + mDefaultFilter.setSupportsEdit(false); + } + } else if (mFilterMode == FILTER_NONE) { + // if the filtering mode is "none", we create a single filter that + // will receive all + mDefaultFilter = new LogFilter("Log"); + mDefaultFilter.setColors(mColors); + mDefaultFilter.setSupportsDelete(false); + mDefaultFilter.setSupportsEdit(false); + } + } + + /** Checks if there's an automatic filter for this md and if not + * adds the filter and the ui. + * This must be called from the UI! + * @param md + * @return true if the filter existed already + */ + private boolean checkFilter(final LogMessageInfo md) { + if (true) + return true; + // look for a filter that matches the pid + if (mFilterMode == FILTER_AUTO_PID) { + for (LogFilter f : mFilters) { + if (f.getPidFilter() == md.pid) { + return true; + } + } + } else if (mFilterMode == FILTER_AUTO_TAG) { + for (LogFilter f : mFilters) { + if (f.getTagFilter().equals(md.tag)) { + return true; + } + } + } + + // if we reach this point, no filter was found. + // create a filter with a temporary name of the pid + final LogFilter newFilter = new LogFilter(md.pidString); + String name = null; + if (mFilterMode == FILTER_AUTO_PID) { + newFilter.setPidMode(md.pid); + + // ask the monitor thread if it knows the pid. + name = mCurrentLoggedDevice.getClientName(md.pid); + } else { + newFilter.setTagMode(md.tag); + name = md.tag; + } + addFilterToArray(newFilter); + + final String fname = name; + + // create the tabitem + final TabItem newTabItem = createTab(newFilter, -1, true); + + // if the name is unknown + if (fname == null) { + // we need to find the process running under that pid. + // launch a thread do a ps on the device + new Thread("remote PS") { //$NON-NLS-1$ + @Override + public void run() { + // create the receiver + PsOutputReceiver psor = new PsOutputReceiver(md.pid, + newFilter, newTabItem); + + // execute ps + try { + mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$ + } catch (IOException e) { + // hmm... + } + } + }.start(); + } + + return false; + } + + /** + * Adds a new filter to the current filter array, and set its colors + * @param newFilter The filter to add + */ + private void addFilterToArray(LogFilter newFilter) { + // set the colors + newFilter.setColors(mColors); + + // add it to the array. + if (mFilters != null && mFilters.length > 0) { + LogFilter[] newFilters = new LogFilter[mFilters.length+1]; + System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length); + newFilters[mFilters.length] = newFilter; + mFilters = newFilters; + } else { + mFilters = new LogFilter[1]; + mFilters[0] = newFilter; + } + } + + private void removeFilterFromArray(LogFilter oldFilter) { + // look for the index + int index = -1; + for (int i = 0 ; i < mFilters.length ; i++) { + if (mFilters[i] == oldFilter) { + index = i; + break; + } + } + + if (index != -1) { + LogFilter[] newFilters = new LogFilter[mFilters.length-1]; + System.arraycopy(mFilters, 0, newFilters, 0, index); + System.arraycopy(mFilters, index + 1, newFilters, index, + newFilters.length-index); + mFilters = newFilters; + } + } + + /** + * Initialize the filter with already existing buffer. + * @param filter + */ + private void initFilter(LogFilter filter) { + // is it empty + if (filter.uiReady() == false) { + return; + } + + if (filter == mDefaultFilter) { + initDefaultFilter(); + return; + } + + filter.clear(); + + if (mBufferStart != -1) { + int max = mBufferEnd; + if (mBufferEnd < mBufferStart) { + max += STRING_BUFFER_LENGTH; + } + + for (int i = mBufferStart; i < max; i++) { + int realItemIndex = i % STRING_BUFFER_LENGTH; + + filter.addMessage(mBuffer[realItemIndex], null /* old message */); + } + } + + filter.flush(); + filter.resetTempFilteringStatus(); + } + + /** + * Refill the default filter. Not to be called directly. + * @see initFilter() + */ + private void initDefaultFilter() { + mDefaultFilter.clear(); + + if (mBufferStart != -1) { + int max = mBufferEnd; + if (mBufferEnd < mBufferStart) { + max += STRING_BUFFER_LENGTH; + } + + for (int i = mBufferStart; i < max; i++) { + int realItemIndex = i % STRING_BUFFER_LENGTH; + LogMessage msg = mBuffer[realItemIndex]; + + // first we check that the other filters don't take this message + boolean filtered = false; + for (LogFilter f : mFilters) { + filtered |= f.accept(msg); + } + + if (filtered == false) { + mDefaultFilter.addMessage(msg, null /* old message */); + } + } + } + + mDefaultFilter.flush(); + mDefaultFilter.resetTempFilteringStatus(); + } + + /** + * Reset the filters, to handle change in device in automatic filter mode + */ + private void resetFilters() { + // if we are in automatic mode, then we need to rmove the current + // filter. + if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) { + mFilters = null; + + // recreate the filters. + createFilters(); + } + } + + + private LogFilter getCurrentFilter() { + int index = mFolders.getSelectionIndex(); + + // if mFilters is null or index is invalid, we return the default + // filter. It doesn't matter if that one is null as well, since we + // would return null anyway. + if (index == 0 || mFilters == null) { + return mDefaultFilter; + } + + return mFilters[index-1]; + } + + + private void emptyTables() { + for (LogFilter f : mFilters) { + f.getTable().removeAll(); + } + + if (mDefaultFilter != null) { + mDefaultFilter.getTable().removeAll(); + } + } + + protected void updateFilteringWith(String text) { + synchronized (mBuffer) { + // reset the temp filtering for all the filters + for (LogFilter f : mFilters) { + f.resetTempFiltering(); + } + if (mDefaultFilter != null) { + mDefaultFilter.resetTempFiltering(); + } + + // now we need to figure out the new temp filtering + // split each word + String[] segments = text.split(" "); //$NON-NLS-1$ + + ArrayList<String> keywords = new ArrayList<String>(segments.length); + + // loop and look for temp id/tag + int tempPid = -1; + String tempTag = null; + for (int i = 0 ; i < segments.length; i++) { + String s = segments[i]; + if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$ + // get the pid + String[] seg = s.split(":"); //$NON-NLS-1$ + if (seg.length == 2) { + if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$ + tempPid = Integer.valueOf(seg[1]); + } + } + } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$ + String seg[] = segments[i].split(":"); //$NON-NLS-1$ + if (seg.length == 2) { + tempTag = seg[1]; + } + } else { + keywords.add(s); + } + } + + // set the temp filtering in the filters + if (tempPid != -1 || tempTag != null || keywords.size() > 0) { + String[] keywordsArray = keywords.toArray( + new String[keywords.size()]); + + for (LogFilter f : mFilters) { + if (tempPid != -1) { + f.setTempPidFiltering(tempPid); + } + if (tempTag != null) { + f.setTempTagFiltering(tempTag); + } + f.setTempKeywordFiltering(keywordsArray); + } + + if (mDefaultFilter != null) { + if (tempPid != -1) { + mDefaultFilter.setTempPidFiltering(tempPid); + } + if (tempTag != null) { + mDefaultFilter.setTempTagFiltering(tempTag); + } + mDefaultFilter.setTempKeywordFiltering(keywordsArray); + + } + } + + initFilter(mCurrentFilter); + } + } + + /** + * Called when the current filter selection changes. + * @param selectedFilter + */ + private void selectionChanged(LogFilter selectedFilter) { + if (mLogLevelActions != null) { + // get the log level + int level = selectedFilter.getLogLevel(); + for (int i = 0 ; i < mLogLevelActions.length; i++) { + ICommonAction a = mLogLevelActions[i]; + if (i == level - 2) { + a.setChecked(true); + } else { + a.setChecked(false); + } + } + } + + if (mDeleteFilterAction != null) { + mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete()); + } + if (mEditFilterAction != null) { + mEditFilterAction.setEnabled(selectedFilter.supportsEdit()); + } + } +} diff --git a/ddms/libs/ddmuilib/src/resources/images/add.png b/ddms/libs/ddmuilib/src/resources/images/add.png Binary files differnew file mode 100644 index 0000000..eefc2ca --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/add.png diff --git a/ddms/libs/ddmuilib/src/resources/images/android.png b/ddms/libs/ddmuilib/src/resources/images/android.png Binary files differnew file mode 100644 index 0000000..3779d4d --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/android.png diff --git a/ddms/libs/ddmuilib/src/resources/images/backward.png b/ddms/libs/ddmuilib/src/resources/images/backward.png Binary files differnew file mode 100644 index 0000000..90a9713 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/backward.png diff --git a/ddms/libs/ddmuilib/src/resources/images/clear.png b/ddms/libs/ddmuilib/src/resources/images/clear.png Binary files differnew file mode 100644 index 0000000..0009cf6 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/clear.png diff --git a/ddms/libs/ddmuilib/src/resources/images/d.png b/ddms/libs/ddmuilib/src/resources/images/d.png Binary files differnew file mode 100644 index 0000000..d45506e --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/d.png diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-attach.png b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png Binary files differnew file mode 100644 index 0000000..9b8a11c --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-error.png b/ddms/libs/ddmuilib/src/resources/images/debug-error.png Binary files differnew file mode 100644 index 0000000..f22da1f --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/debug-error.png diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-wait.png b/ddms/libs/ddmuilib/src/resources/images/debug-wait.png Binary files differnew file mode 100644 index 0000000..322be63 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/debug-wait.png diff --git a/ddms/libs/ddmuilib/src/resources/images/delete.png b/ddms/libs/ddmuilib/src/resources/images/delete.png Binary files differnew file mode 100644 index 0000000..db5fab8 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/delete.png diff --git a/ddms/libs/ddmuilib/src/resources/images/device.png b/ddms/libs/ddmuilib/src/resources/images/device.png Binary files differnew file mode 100644 index 0000000..7dbbbb6 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/device.png diff --git a/ddms/libs/ddmuilib/src/resources/images/down.png b/ddms/libs/ddmuilib/src/resources/images/down.png Binary files differnew file mode 100644 index 0000000..f9426cb --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/down.png diff --git a/ddms/libs/ddmuilib/src/resources/images/e.png b/ddms/libs/ddmuilib/src/resources/images/e.png Binary files differnew file mode 100644 index 0000000..dee7c97 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/e.png diff --git a/ddms/libs/ddmuilib/src/resources/images/edit.png b/ddms/libs/ddmuilib/src/resources/images/edit.png Binary files differnew file mode 100644 index 0000000..b8f65bc --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/edit.png diff --git a/ddms/libs/ddmuilib/src/resources/images/empty.png b/ddms/libs/ddmuilib/src/resources/images/empty.png Binary files differnew file mode 100644 index 0000000..f021542 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/empty.png diff --git a/ddms/libs/ddmuilib/src/resources/images/emulator.png b/ddms/libs/ddmuilib/src/resources/images/emulator.png Binary files differnew file mode 100644 index 0000000..a718042 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/emulator.png diff --git a/ddms/libs/ddmuilib/src/resources/images/file.png b/ddms/libs/ddmuilib/src/resources/images/file.png Binary files differnew file mode 100644 index 0000000..043a814 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/file.png diff --git a/ddms/libs/ddmuilib/src/resources/images/folder.png b/ddms/libs/ddmuilib/src/resources/images/folder.png Binary files differnew file mode 100644 index 0000000..7e29b1a --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/folder.png diff --git a/ddms/libs/ddmuilib/src/resources/images/forward.png b/ddms/libs/ddmuilib/src/resources/images/forward.png Binary files differnew file mode 100644 index 0000000..a97a605 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/forward.png diff --git a/ddms/libs/ddmuilib/src/resources/images/gc.png b/ddms/libs/ddmuilib/src/resources/images/gc.png Binary files differnew file mode 100644 index 0000000..5194806 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/gc.png diff --git a/ddms/libs/ddmuilib/src/resources/images/halt.png b/ddms/libs/ddmuilib/src/resources/images/halt.png Binary files differnew file mode 100644 index 0000000..10e3720 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/halt.png diff --git a/ddms/libs/ddmuilib/src/resources/images/heap.png b/ddms/libs/ddmuilib/src/resources/images/heap.png Binary files differnew file mode 100644 index 0000000..e3aa3f0 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/heap.png diff --git a/ddms/libs/ddmuilib/src/resources/images/i.png b/ddms/libs/ddmuilib/src/resources/images/i.png Binary files differnew file mode 100644 index 0000000..98385c5 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/i.png diff --git a/ddms/libs/ddmuilib/src/resources/images/importBug.png b/ddms/libs/ddmuilib/src/resources/images/importBug.png Binary files differnew file mode 100644 index 0000000..f5da179 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/importBug.png diff --git a/ddms/libs/ddmuilib/src/resources/images/load.png b/ddms/libs/ddmuilib/src/resources/images/load.png Binary files differnew file mode 100644 index 0000000..9e7bf6e --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/load.png diff --git a/ddms/libs/ddmuilib/src/resources/images/pause.png b/ddms/libs/ddmuilib/src/resources/images/pause.png Binary files differnew file mode 100644 index 0000000..19d286d --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/pause.png diff --git a/ddms/libs/ddmuilib/src/resources/images/play.png b/ddms/libs/ddmuilib/src/resources/images/play.png Binary files differnew file mode 100644 index 0000000..d54f013 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/play.png diff --git a/ddms/libs/ddmuilib/src/resources/images/pull.png b/ddms/libs/ddmuilib/src/resources/images/pull.png Binary files differnew file mode 100644 index 0000000..f48f1b1 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/pull.png diff --git a/ddms/libs/ddmuilib/src/resources/images/push.png b/ddms/libs/ddmuilib/src/resources/images/push.png Binary files differnew file mode 100644 index 0000000..6222864 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/push.png diff --git a/ddms/libs/ddmuilib/src/resources/images/save.png b/ddms/libs/ddmuilib/src/resources/images/save.png Binary files differnew file mode 100644 index 0000000..040ebda --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/save.png diff --git a/ddms/libs/ddmuilib/src/resources/images/thread.png b/ddms/libs/ddmuilib/src/resources/images/thread.png Binary files differnew file mode 100644 index 0000000..ac839e8 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/thread.png diff --git a/ddms/libs/ddmuilib/src/resources/images/up.png b/ddms/libs/ddmuilib/src/resources/images/up.png Binary files differnew file mode 100644 index 0000000..92edf5a --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/up.png diff --git a/ddms/libs/ddmuilib/src/resources/images/v.png b/ddms/libs/ddmuilib/src/resources/images/v.png Binary files differnew file mode 100644 index 0000000..8044051 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/v.png diff --git a/ddms/libs/ddmuilib/src/resources/images/w.png b/ddms/libs/ddmuilib/src/resources/images/w.png Binary files differnew file mode 100644 index 0000000..129d0f9 --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/w.png diff --git a/ddms/libs/ddmuilib/src/resources/images/warning.png b/ddms/libs/ddmuilib/src/resources/images/warning.png Binary files differnew file mode 100644 index 0000000..ca3b6ed --- /dev/null +++ b/ddms/libs/ddmuilib/src/resources/images/warning.png |