blob: 154e2fabbcc997e5145510c20a232099e66d9adb [file] [log] [blame]
/*
* 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.IDevice;
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 &lt;pid&gt;:0x&lt;???&gt; &lt;severity&gt;/&lt;tag&gt;]"</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 IDevice 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 IDevice 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());
}
}
}