| /* |
| * 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 <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 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()); |
| } |
| } |
| } |