2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.ddmuilib.logcat;
19 import com.android.ddmlib.IDevice;
20 import com.android.ddmlib.Log;
21 import com.android.ddmlib.MultiLineReceiver;
22 import com.android.ddmlib.Log.LogLevel;
23 import com.android.ddmuilib.DdmUiPreferences;
24 import com.android.ddmuilib.ITableFocusListener;
25 import com.android.ddmuilib.SelectionDependentPanel;
26 import com.android.ddmuilib.TableHelper;
27 import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
28 import com.android.ddmuilib.actions.ICommonAction;
30 import org.eclipse.jface.preference.IPreferenceStore;
31 import org.eclipse.swt.SWT;
32 import org.eclipse.swt.SWTException;
33 import org.eclipse.swt.dnd.Clipboard;
34 import org.eclipse.swt.dnd.TextTransfer;
35 import org.eclipse.swt.dnd.Transfer;
36 import org.eclipse.swt.events.ControlEvent;
37 import org.eclipse.swt.events.ControlListener;
38 import org.eclipse.swt.events.FocusEvent;
39 import org.eclipse.swt.events.FocusListener;
40 import org.eclipse.swt.events.ModifyEvent;
41 import org.eclipse.swt.events.ModifyListener;
42 import org.eclipse.swt.events.SelectionAdapter;
43 import org.eclipse.swt.events.SelectionEvent;
44 import org.eclipse.swt.graphics.Font;
45 import org.eclipse.swt.graphics.Rectangle;
46 import org.eclipse.swt.layout.FillLayout;
47 import org.eclipse.swt.layout.GridData;
48 import org.eclipse.swt.layout.GridLayout;
49 import org.eclipse.swt.widgets.Composite;
50 import org.eclipse.swt.widgets.Control;
51 import org.eclipse.swt.widgets.Display;
52 import org.eclipse.swt.widgets.FileDialog;
53 import org.eclipse.swt.widgets.Label;
54 import org.eclipse.swt.widgets.TabFolder;
55 import org.eclipse.swt.widgets.TabItem;
56 import org.eclipse.swt.widgets.Table;
57 import org.eclipse.swt.widgets.TableColumn;
58 import org.eclipse.swt.widgets.TableItem;
59 import org.eclipse.swt.widgets.Text;
61 import java.io.FileWriter;
62 import java.io.IOException;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.regex.Matcher;
66 import java.util.regex.Pattern;
68 public class LogPanel extends SelectionDependentPanel {
70 private static final int STRING_BUFFER_LENGTH = 10000;
72 /** no filtering. Only one tab with everything. */
73 public static final int FILTER_NONE = 0;
74 /** manual mode for filter. all filters are manually created. */
75 public static final int FILTER_MANUAL = 1;
76 /** automatic mode for filter (pid mode).
77 * All filters are automatically created. */
78 public static final int FILTER_AUTO_PID = 2;
79 /** automatic mode for filter (tag mode).
80 * All filters are automatically created. */
81 public static final int FILTER_AUTO_TAG = 3;
82 /** Manual filtering mode + new filter for debug app, if needed */
83 public static final int FILTER_DEBUG = 4;
85 public static final int COLUMN_MODE_MANUAL = 0;
86 public static final int COLUMN_MODE_AUTO = 1;
88 public static String PREFS_TIME;
89 public static String PREFS_LEVEL;
90 public static String PREFS_PID;
91 public static String PREFS_TAG;
92 public static String PREFS_MESSAGE;
95 * This pattern is meant to parse the first line of a log message with the option
96 * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the
97 * following lines are the message (can be several line).<br>
98 * This first line looks something like<br>
99 * <code>"[ 00-00 00:00:00.000 <pid>:0x<???> <severity>/<tag>]"</code>
101 * Note: severity is one of V, D, I, W, or EM<br>
102 * Note: the fraction of second value can have any number of digit.
103 * Note the tag should be trim as it may have spaces at the end.
105 private static Pattern sLogPattern = Pattern.compile(
106 "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + //$NON-NLS-1$
107 "\\s+(\\d*):(0x[0-9a-fA-F]+)\\s([VDIWE])/(.*)\\]$"); //$NON-NLS-1$
110 * Interface for Storage Filter manager. Implementation of this interface
111 * provide a custom way to archive an reload filters.
113 public interface ILogFilterStorageManager {
115 public LogFilter[] getFilterFromStore();
117 public void saveFilters(LogFilter[] filters);
119 public boolean requiresDefaultFilter();
122 private Composite mParent;
123 private IPreferenceStore mStore;
125 /** top object in the view */
126 private TabFolder mFolders;
128 private LogColors mColors;
130 private ILogFilterStorageManager mFilterStorage;
132 private LogCatOuputReceiver mCurrentLogCat;
135 * Circular buffer containing the logcat output. This is unfiltered.
136 * The valid content goes from <code>mBufferStart</code> to
137 * <code>mBufferEnd - 1</code>. Therefore its number of item is
138 * <code>mBufferEnd - mBufferStart</code>.
140 private LogMessage[] mBuffer = new LogMessage[STRING_BUFFER_LENGTH];
142 /** Represents the oldest message in the buffer */
143 private int mBufferStart = -1;
146 * Represents the next usable item in the buffer to receive new message.
147 * This can be equal to mBufferStart, but when used mBufferStart will be
148 * incremented as well.
150 private int mBufferEnd = -1;
153 private LogFilter[] mFilters;
155 /** Default filter */
156 private LogFilter mDefaultFilter;
158 /** Current filter being displayed */
159 private LogFilter mCurrentFilter;
161 /** Filtering mode */
162 private int mFilterMode = FILTER_NONE;
164 /** Device currently running logcat */
165 private IDevice mCurrentLoggedDevice = null;
167 private ICommonAction mDeleteFilterAction;
168 private ICommonAction mEditFilterAction;
170 private ICommonAction[] mLogLevelActions;
172 /** message data, separated from content for multi line messages */
173 protected static class LogMessageInfo {
174 public LogLevel logLevel;
176 public String pidString;
181 /** pointer to the latest LogMessageInfo. this is used for multi line
182 * log message, to reuse the info regarding level, pid, etc...
184 private LogMessageInfo mLastMessageInfo = null;
186 private boolean mPendingAsyncRefresh = false;
188 private String mDefaultLogSave;
190 private int mColumnMode = COLUMN_MODE_MANUAL;
191 private Font mDisplayFont;
193 private ITableFocusListener mGlobalListener;
195 /** message data, separated from content for multi line messages */
196 protected static class LogMessage {
197 public LogMessageInfo data;
201 public String toString() {
202 return data.time + ": " //$NON-NLS-1$
203 + data.logLevel + "/" //$NON-NLS-1$
204 + data.tag + "(" //$NON-NLS-1$
205 + data.pidString + "): " //$NON-NLS-1$
211 * objects able to receive the output of a remote shell command,
212 * specifically a logcat command in this case
214 private final class LogCatOuputReceiver extends MultiLineReceiver {
216 public boolean isCancelled = false;
218 public LogCatOuputReceiver() {
225 public void processNewLines(String[] lines) {
226 if (isCancelled == false) {
227 processLogLines(lines);
231 public boolean isCancelled() {
237 * Parser class for the output of a "ps" shell command executed on a device.
238 * This class looks for a specific pid to find the process name from it.
239 * Once found, the name is used to update a filter and a tab object
242 private class PsOutputReceiver extends MultiLineReceiver {
244 private LogFilter mFilter;
246 private TabItem mTabItem;
250 /** set to true when we've found the pid we're looking for */
251 private boolean mDone = false;
253 PsOutputReceiver(int pid, LogFilter filter, TabItem tabItem) {
259 public boolean isCancelled() {
264 public void processNewLines(String[] lines) {
265 for (String line : lines) {
266 if (line.startsWith("USER")) { //$NON-NLS-1$
270 int index = line.indexOf(' ');
274 // look for the next non blank char
276 while (line.charAt(index) == ' ') {
280 // this is the start of the pid.
282 int index2 = line.indexOf(' ', index);
285 String pidStr = line.substring(index, index2);
286 int pid = Integer.parseInt(pidStr);
290 // get the process name
291 index = line.lastIndexOf(' ');
292 final String name = line.substring(index + 1);
294 mFilter.setName(name);
297 Display d = mFolders.getDisplay();
298 d.asyncExec(new Runnable() {
300 mTabItem.setText(name);
304 // we're done with this ps.
315 * Create the log view with some default parameters
316 * @param colors The display color object
317 * @param filterStorage the storage for user defined filters.
318 * @param mode The filtering mode
320 public LogPanel(LogColors colors,
321 ILogFilterStorageManager filterStorage, int mode) {
324 mFilterStorage = filterStorage;
325 mStore = DdmUiPreferences.getStore();
328 public void setActions(ICommonAction deleteAction, ICommonAction editAction,
329 ICommonAction[] logLevelActions) {
330 mDeleteFilterAction = deleteAction;
331 mEditFilterAction = editAction;
332 mLogLevelActions = logLevelActions;
336 * Sets the column mode. Must be called before creatUI
337 * @param mode the column mode. Valid values are COLUMN_MOD_MANUAL and
340 public void setColumnMode(int mode) {
345 * Sets the display font.
346 * @param font The display font.
348 public void setFont(Font font) {
351 if (mFilters != null) {
352 for (LogFilter f : mFilters) {
353 Table table = f.getTable();
360 if (mDefaultFilter != null) {
361 Table table = mDefaultFilter.getTable();
369 * Sent when a new device is selected. The new device can be accessed
370 * with {@link #getCurrentDevice()}.
373 public void deviceSelected() {
374 startLogCat(getCurrentDevice());
378 * Sent when a new client is selected. The new client can be accessed
379 * with {@link #getCurrentClient()}.
382 public void clientSelected() {
388 * Creates a control capable of displaying some information. This is
389 * called once, when the application is initializing, from the UI thread.
392 protected Control createControl(Composite parent) {
395 Composite top = new Composite(parent, SWT.NONE);
396 top.setLayoutData(new GridData(GridData.FILL_BOTH));
397 top.setLayout(new GridLayout(1, false));
399 // create the tab folder
400 mFolders = new TabFolder(top, SWT.NONE);
401 mFolders.setLayoutData(new GridData(GridData.FILL_BOTH));
402 mFolders.addSelectionListener(new SelectionAdapter() {
404 public void widgetSelected(SelectionEvent e) {
405 if (mCurrentFilter != null) {
406 mCurrentFilter.setSelectedState(false);
408 mCurrentFilter = getCurrentFilter();
409 mCurrentFilter.setSelectedState(true);
410 updateColumns(mCurrentFilter.getTable());
411 if (mCurrentFilter.getTempFilterStatus()) {
412 initFilter(mCurrentFilter);
414 selectionChanged(mCurrentFilter);
419 Composite bottom = new Composite(top, SWT.NONE);
420 bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
421 bottom.setLayout(new GridLayout(3, false));
423 Label label = new Label(bottom, SWT.NONE);
424 label.setText("Filter:");
426 final Text filterText = new Text(bottom, SWT.SINGLE | SWT.BORDER);
427 filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
428 filterText.addModifyListener(new ModifyListener() {
429 public void modifyText(ModifyEvent e) {
430 updateFilteringWith(filterText.getText());
435 Button addFilterBtn = new Button(bottom, SWT.NONE);
436 addFilterBtn.setImage(mImageLoader.loadImage("add.png", //$NON-NLS-1$
437 addFilterBtn.getDisplay()));
443 // for each filter, create a tab.
446 if (mDefaultFilter != null) {
447 createTab(mDefaultFilter, index++, false);
450 if (mFilters != null) {
451 for (LogFilter f : mFilters) {
452 createTab(f, index++, false);
460 protected void postCreation() {
465 * Sets the focus to the proper object.
468 public void setFocus() {
474 * Starts a new logcat and set mCurrentLogCat as the current receiver.
475 * @param device the device to connect logcat to.
477 public void startLogCat(final IDevice device) {
478 if (device == mCurrentLoggedDevice) {
482 // if we have a logcat already running
483 if (mCurrentLoggedDevice != null) {
485 mCurrentLoggedDevice = null;
490 if (device != null) {
491 // create a new output receiver
492 mCurrentLogCat = new LogCatOuputReceiver();
494 // start the logcat in a different thread
495 new Thread("Logcat") { //$NON-NLS-1$
499 while (device.isOnline() == false &&
500 mCurrentLogCat != null &&
501 mCurrentLogCat.isCancelled == false) {
504 } catch (InterruptedException e) {
509 if (mCurrentLogCat == null || mCurrentLogCat.isCancelled) {
510 // logcat was stopped/cancelled before the device became ready.
515 mCurrentLoggedDevice = device;
516 device.executeShellCommand("logcat -v long", mCurrentLogCat, 0 /*timeout*/); //$NON-NLS-1$
517 } catch (Exception e) {
520 // at this point the command is terminated.
521 mCurrentLogCat = null;
522 mCurrentLoggedDevice = null;
529 /** Stop the current logcat */
530 public void stopLogCat(boolean inUiThread) {
531 if (mCurrentLogCat != null) {
532 mCurrentLogCat.isCancelled = true;
534 // when the thread finishes, no one will reference that object
535 // and it'll be destroyed
536 mCurrentLogCat = null;
538 // reset the content buffer
539 for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
543 // because it's a circular buffer, it's hard to know if
544 // the array is empty with both start/end at 0 or if it's full
545 // with both start/end at 0 as well. So to mean empty, we use -1
555 * Adds a new Filter. This methods displays the UI to create the filter
556 * and set up its parameters.<br>
557 * <b>MUST</b> be called from the ui thread.
560 public void addFilter() {
561 EditFilterDialog dlg = new EditFilterDialog(mFolders.getShell());
563 synchronized (mBuffer) {
564 // get the new filter in the array
565 LogFilter filter = dlg.getFilter();
566 addFilterToArray(filter);
568 int index = mFilters.length - 1;
569 if (mDefaultFilter != null) {
575 for (LogFilter f : mFilters) {
580 if (mDefaultFilter != null && mDefaultFilter.uiReady()) {
581 mDefaultFilter.dispose();
584 // for each filter, create a tab.
586 if (mFilters != null) {
587 for (LogFilter f : mFilters) {
588 createTab(f, i++, true);
591 if (mDefaultFilter != null) {
592 createTab(mDefaultFilter, i++, true);
596 // create ui for the filter.
597 createTab(filter, index, true);
599 // reset the default as it shouldn't contain the content of
601 if (mDefaultFilter != null) {
606 // select the new filter
607 if (mCurrentFilter != null) {
608 mCurrentFilter.setSelectedState(false);
610 mFolders.setSelection(index);
611 filter.setSelectedState(true);
612 mCurrentFilter = filter;
614 selectionChanged(filter);
616 // finally we update the filtering mode if needed
617 if (mFilterMode == FILTER_NONE) {
618 mFilterMode = FILTER_MANUAL;
621 mFilterStorage.saveFilters(mFilters);
628 * Edits the current filter. The method displays the UI to edit the filter.
630 public void editFilter() {
631 if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
632 EditFilterDialog dlg = new EditFilterDialog(
633 mFolders.getShell(), mCurrentFilter);
635 synchronized (mBuffer) {
636 // at this point the filter has been updated.
637 // so we update its content
638 initFilter(mCurrentFilter);
640 // and the content of the "other" filter as well.
641 if (mDefaultFilter != null) {
645 mFilterStorage.saveFilters(mFilters);
652 * Deletes the current filter.
654 public void deleteFilter() {
655 synchronized (mBuffer) {
656 if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
657 // remove the filter from the list
658 removeFilterFromArray(mCurrentFilter);
659 mCurrentFilter.dispose();
661 // select the new filter
662 mFolders.setSelection(0);
663 if (mFilters.length > 0) {
664 mCurrentFilter = mFilters[0];
666 mCurrentFilter = mDefaultFilter;
669 selectionChanged(mCurrentFilter);
671 // update the content of the "other" filter to include what was filtered out
672 // by the deleted filter.
673 if (mDefaultFilter != null) {
677 mFilterStorage.saveFilters(mFilters);
683 * saves the current selection in a text file.
684 * @return false if the saving failed.
686 public boolean save() {
687 synchronized (mBuffer) {
688 FileDialog dlg = new FileDialog(mParent.getShell(), SWT.SAVE);
691 dlg.setText("Save log...");
692 dlg.setFileName("log.txt");
693 String defaultPath = mDefaultLogSave;
694 if (defaultPath == null) {
695 defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
697 dlg.setFilterPath(defaultPath);
698 dlg.setFilterNames(new String[] {
701 dlg.setFilterExtensions(new String[] {
705 fileName = dlg.open();
706 if (fileName != null) {
707 mDefaultLogSave = dlg.getFilterPath();
709 // get the current table and its selection
710 Table currentTable = mCurrentFilter.getTable();
712 int[] selection = currentTable.getSelectionIndices();
714 // we need to sort the items to be sure.
715 Arrays.sort(selection);
717 // loop on the selection and output the file.
719 FileWriter writer = new FileWriter(fileName);
721 for (int i : selection) {
722 TableItem item = currentTable.getItem(i);
723 LogMessage msg = (LogMessage)item.getData();
724 String line = msg.toString();
730 } catch (IOException e) {
740 * Empty the current circular buffer.
742 public void clear() {
743 synchronized (mBuffer) {
744 for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
751 // now we clear the existing filters
752 for (LogFilter filter : mFilters) {
756 // and the default one
757 if (mDefaultFilter != null) {
758 mDefaultFilter.clear();
764 * Copies the current selection of the current filter as multiline text.
766 * @param clipboard The clipboard to place the copied content.
768 public void copy(Clipboard clipboard) {
769 // get the current table and its selection
770 Table currentTable = mCurrentFilter.getTable();
772 copyTable(clipboard, currentTable);
778 public void selectAll() {
779 Table currentTable = mCurrentFilter.getTable();
780 currentTable.selectAll();
784 * Sets a TableFocusListener which will be notified when one of the tables
785 * gets or loses focus.
789 public void setTableFocusListener(ITableFocusListener listener) {
790 // record the global listener, to make sure table created after
791 // this call will still be setup.
792 mGlobalListener = listener;
794 // now we setup the existing filters
795 for (LogFilter filter : mFilters) {
796 Table table = filter.getTable();
798 addTableToFocusListener(table);
801 // and the default one
802 if (mDefaultFilter != null) {
803 addTableToFocusListener(mDefaultFilter.getTable());
808 * Sets up a Table object to notify the global Table Focus listener when it
809 * gets or loses the focus.
811 * @param table the Table object.
813 private void addTableToFocusListener(final Table table) {
814 // create the activator for this table
815 final IFocusedTableActivator activator = new IFocusedTableActivator() {
816 public void copy(Clipboard clipboard) {
817 copyTable(clipboard, table);
820 public void selectAll() {
825 // add the focus listener on the table to notify the global listener
826 table.addFocusListener(new FocusListener() {
827 public void focusGained(FocusEvent e) {
828 mGlobalListener.focusGained(activator);
831 public void focusLost(FocusEvent e) {
832 mGlobalListener.focusLost(activator);
838 * Copies the current selection of a Table into the provided Clipboard, as
841 * @param clipboard The clipboard to place the copied content.
842 * @param table The table to copy from.
844 private static void copyTable(Clipboard clipboard, Table table) {
845 int[] selection = table.getSelectionIndices();
847 // we need to sort the items to be sure.
848 Arrays.sort(selection);
850 // all lines must be concatenated.
851 StringBuilder sb = new StringBuilder();
853 // loop on the selection and output the file.
854 for (int i : selection) {
855 TableItem item = table.getItem(i);
856 LogMessage msg = (LogMessage)item.getData();
857 String line = msg.toString();
862 // now add that to the clipboard
863 clipboard.setContents(new Object[] {
866 TextTransfer.getInstance()
871 * Sets the log level for the current filter, but does not save it.
874 public void setCurrentFilterLogLevel(int i) {
875 LogFilter filter = getCurrentFilter();
877 filter.setLogLevel(i);
883 * Creates a new tab in the folderTab item. Must be called from the ui
885 * @param filter The filter associated with the tab.
886 * @param index the index of the tab. if -1, the tab will be added at the
888 * @param fillTable If true the table is filled with the current content of
890 * @return The TabItem object that was created.
892 private TabItem createTab(LogFilter filter, int index, boolean fillTable) {
893 synchronized (mBuffer) {
896 item = new TabItem(mFolders, SWT.NONE, index);
898 item = new TabItem(mFolders, SWT.NONE);
900 item.setText(filter.getName());
902 // set the control (the parent is the TabFolder item, always)
903 Composite top = new Composite(mFolders, SWT.NONE);
904 item.setControl(top);
906 top.setLayout(new FillLayout());
908 // create the ui, first the table
909 final Table t = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
911 if (mDisplayFont != null) {
912 t.setFont(mDisplayFont);
915 // give the ui objects to the filters.
916 filter.setWidgets(item, t);
918 t.setHeaderVisible(true);
919 t.setLinesVisible(false);
921 if (mGlobalListener != null) {
922 addTableToFocusListener(t);
925 // create a controllistener that will handle the resizing of all the
926 // columns (except the last) and of the table itself.
927 ControlListener listener = null;
928 if (mColumnMode == COLUMN_MODE_AUTO) {
929 listener = new ControlListener() {
930 public void controlMoved(ControlEvent e) {
933 public void controlResized(ControlEvent e) {
934 Rectangle r = t.getClientArea();
936 // get the size of all but the last column
937 int total = t.getColumn(0).getWidth();
938 total += t.getColumn(1).getWidth();
939 total += t.getColumn(2).getWidth();
940 total += t.getColumn(3).getWidth();
942 if (r.width > total) {
943 t.getColumn(4).setWidth(r.width-total);
948 t.addControlListener(listener);
952 TableColumn col = TableHelper.createTableColumn(t, "Time", SWT.LEFT,
953 "00-00 00:00:00", //$NON-NLS-1$
955 if (mColumnMode == COLUMN_MODE_AUTO) {
956 col.addControlListener(listener);
959 col = TableHelper.createTableColumn(t, "", SWT.CENTER,
961 PREFS_LEVEL, mStore);
962 if (mColumnMode == COLUMN_MODE_AUTO) {
963 col.addControlListener(listener);
966 col = TableHelper.createTableColumn(t, "pid", SWT.LEFT,
967 "9999", //$NON-NLS-1$
969 if (mColumnMode == COLUMN_MODE_AUTO) {
970 col.addControlListener(listener);
973 col = TableHelper.createTableColumn(t, "tag", SWT.LEFT,
974 "abcdefgh", //$NON-NLS-1$
976 if (mColumnMode == COLUMN_MODE_AUTO) {
977 col.addControlListener(listener);
980 col = TableHelper.createTableColumn(t, "Message", SWT.LEFT,
981 "abcdefghijklmnopqrstuvwxyz0123456789", //$NON-NLS-1$
982 PREFS_MESSAGE, mStore);
983 if (mColumnMode == COLUMN_MODE_AUTO) {
984 // instead of listening on resize for the last column, we make
986 col.setResizable(false);
996 protected void updateColumns(Table table) {
1001 col = table.getColumn(index++);
1002 col.setWidth(mStore.getInt(PREFS_TIME));
1004 col = table.getColumn(index++);
1005 col.setWidth(mStore.getInt(PREFS_LEVEL));
1007 col = table.getColumn(index++);
1008 col.setWidth(mStore.getInt(PREFS_PID));
1010 col = table.getColumn(index++);
1011 col.setWidth(mStore.getInt(PREFS_TAG));
1013 col = table.getColumn(index++);
1014 col.setWidth(mStore.getInt(PREFS_MESSAGE));
1018 public void resetUI(boolean inUiThread) {
1019 if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
1023 createControl(mParent);
1025 Display d = mFolders.getDisplay();
1027 // run sync as we need to update right now.
1028 d.syncExec(new Runnable() {
1032 createControl(mParent);
1037 // the ui is static we just empty it.
1038 if (mFolders.isDisposed() == false) {
1042 Display d = mFolders.getDisplay();
1044 // run sync as we need to update right now.
1045 d.syncExec(new Runnable() {
1047 if (mFolders.isDisposed() == false) {
1058 * Process new Log lines coming from {@link LogCatOuputReceiver}.
1059 * @param lines the new lines
1061 protected void processLogLines(String[] lines) {
1062 // WARNING: this will not work if the string contains more line than
1063 // the buffer holds.
1065 if (lines.length > STRING_BUFFER_LENGTH) {
1066 Log.e("LogCat", "Receiving more lines than STRING_BUFFER_LENGTH");
1069 // parse the lines and create LogMessage that are stored in a temporary list
1070 final ArrayList<LogMessage> newMessages = new ArrayList<LogMessage>();
1072 synchronized (mBuffer) {
1073 for (String line : lines) {
1074 // ignore empty lines.
1075 if (line.length() > 0) {
1076 // check for header lines.
1077 Matcher matcher = sLogPattern.matcher(line);
1078 if (matcher.matches()) {
1079 // this is a header line, parse the header and keep it around.
1080 mLastMessageInfo = new LogMessageInfo();
1082 mLastMessageInfo.time = matcher.group(1);
1083 mLastMessageInfo.pidString = matcher.group(2);
1084 mLastMessageInfo.pid = Integer.valueOf(mLastMessageInfo.pidString);
1085 mLastMessageInfo.logLevel = LogLevel.getByLetterString(matcher.group(4));
1086 mLastMessageInfo.tag = matcher.group(5).trim();
1088 // This is not a header line.
1089 // Create a new LogMessage and process it.
1090 LogMessage mc = new LogMessage();
1092 if (mLastMessageInfo == null) {
1093 // The first line of output wasn't preceded
1094 // by a header line; make something up so
1095 // that users of mc.data don't NPE.
1096 mLastMessageInfo = new LogMessageInfo();
1097 mLastMessageInfo.time = "??-?? ??:??:??.???"; //$NON-NLS1$
1098 mLastMessageInfo.pidString = "<unknown>"; //$NON-NLS1$
1099 mLastMessageInfo.pid = 0;
1100 mLastMessageInfo.logLevel = LogLevel.INFO;
1101 mLastMessageInfo.tag = "<unknown>"; //$NON-NLS1$
1104 // If someone printed a log message with
1105 // embedded '\n' characters, there will
1106 // one header line followed by multiple text lines.
1107 // Use the last header that we saw.
1108 mc.data = mLastMessageInfo;
1110 // tabs seem to display as only 1 tab so we replace the leading tabs
1112 mc.msg = line.replaceAll("\t", " "); //$NON-NLS-1$ //$NON-NLS-2$
1114 // process the new LogMessage.
1115 processNewMessage(mc);
1117 // store the new LogMessage
1118 newMessages.add(mc);
1123 // if we don't have a pending Runnable that will do the refresh, we ask the Display
1124 // to run one in the UI thread.
1125 if (mPendingAsyncRefresh == false) {
1126 mPendingAsyncRefresh = true;
1129 Display display = mFolders.getDisplay();
1131 // run in sync because this will update the buffer start/end indices
1132 display.asyncExec(new Runnable() {
1137 } catch (SWTException e) {
1138 // display is disposed, we're probably quitting. Let's stop.
1146 * Refreshes the UI with new messages.
1148 private void asyncRefresh() {
1149 if (mFolders.isDisposed() == false) {
1150 synchronized (mBuffer) {
1152 // the circular buffer has been updated, let have the filter flush their
1153 // display with the new messages.
1154 if (mFilters != null) {
1155 for (LogFilter f : mFilters) {
1160 if (mDefaultFilter != null) {
1161 mDefaultFilter.flush();
1164 // the pending refresh is done.
1165 mPendingAsyncRefresh = false;
1174 * Processes a new Message.
1175 * <p/>This adds the new message to the buffer, and gives it to the existing filters.
1178 private void processNewMessage(LogMessage newMessage) {
1179 // if we are in auto filtering mode, make sure we have
1180 // a filter for this
1181 if (mFilterMode == FILTER_AUTO_PID ||
1182 mFilterMode == FILTER_AUTO_TAG) {
1183 checkFilter(newMessage.data);
1186 // compute the index where the message goes.
1187 // was the buffer empty?
1188 int messageIndex = -1;
1189 if (mBufferStart == -1) {
1190 messageIndex = mBufferStart = 0;
1193 messageIndex = mBufferEnd;
1195 // check we aren't overwriting start
1196 if (mBufferEnd == mBufferStart) {
1197 mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH;
1200 // increment the next usable slot index
1201 mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH;
1204 LogMessage oldMessage = null;
1206 // record the message that was there before
1207 if (mBuffer[messageIndex] != null) {
1208 oldMessage = mBuffer[messageIndex];
1211 // then add the new one
1212 mBuffer[messageIndex] = newMessage;
1214 // give the new message to every filters.
1215 boolean filtered = false;
1216 if (mFilters != null) {
1217 for (LogFilter f : mFilters) {
1218 filtered |= f.addMessage(newMessage, oldMessage);
1221 if (filtered == false && mDefaultFilter != null) {
1222 mDefaultFilter.addMessage(newMessage, oldMessage);
1226 private void createFilters() {
1227 if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) {
1228 // unarchive the filters.
1229 mFilters = mFilterStorage.getFilterFromStore();
1232 if (mFilters != null) {
1233 for (LogFilter f : mFilters) {
1234 f.setColors(mColors);
1238 if (mFilterStorage.requiresDefaultFilter()) {
1239 mDefaultFilter = new LogFilter("Log");
1240 mDefaultFilter.setColors(mColors);
1241 mDefaultFilter.setSupportsDelete(false);
1242 mDefaultFilter.setSupportsEdit(false);
1244 } else if (mFilterMode == FILTER_NONE) {
1245 // if the filtering mode is "none", we create a single filter that
1247 mDefaultFilter = new LogFilter("Log");
1248 mDefaultFilter.setColors(mColors);
1249 mDefaultFilter.setSupportsDelete(false);
1250 mDefaultFilter.setSupportsEdit(false);
1254 /** Checks if there's an automatic filter for this md and if not
1255 * adds the filter and the ui.
1256 * This must be called from the UI!
1258 * @return true if the filter existed already
1260 private boolean checkFilter(final LogMessageInfo md) {
1263 // look for a filter that matches the pid
1264 if (mFilterMode == FILTER_AUTO_PID) {
1265 for (LogFilter f : mFilters) {
1266 if (f.getPidFilter() == md.pid) {
1270 } else if (mFilterMode == FILTER_AUTO_TAG) {
1271 for (LogFilter f : mFilters) {
1272 if (f.getTagFilter().equals(md.tag)) {
1278 // if we reach this point, no filter was found.
1279 // create a filter with a temporary name of the pid
1280 final LogFilter newFilter = new LogFilter(md.pidString);
1282 if (mFilterMode == FILTER_AUTO_PID) {
1283 newFilter.setPidMode(md.pid);
1285 // ask the monitor thread if it knows the pid.
1286 name = mCurrentLoggedDevice.getClientName(md.pid);
1288 newFilter.setTagMode(md.tag);
1291 addFilterToArray(newFilter);
1293 final String fname = name;
1295 // create the tabitem
1296 final TabItem newTabItem = createTab(newFilter, -1, true);
1298 // if the name is unknown
1299 if (fname == null) {
1300 // we need to find the process running under that pid.
1301 // launch a thread do a ps on the device
1302 new Thread("remote PS") { //$NON-NLS-1$
1305 // create the receiver
1306 PsOutputReceiver psor = new PsOutputReceiver(md.pid,
1307 newFilter, newTabItem);
1311 mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$
1312 } catch (IOException e) {
1323 * Adds a new filter to the current filter array, and set its colors
1324 * @param newFilter The filter to add
1326 private void addFilterToArray(LogFilter newFilter) {
1328 newFilter.setColors(mColors);
1330 // add it to the array.
1331 if (mFilters != null && mFilters.length > 0) {
1332 LogFilter[] newFilters = new LogFilter[mFilters.length+1];
1333 System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length);
1334 newFilters[mFilters.length] = newFilter;
1335 mFilters = newFilters;
1337 mFilters = new LogFilter[1];
1338 mFilters[0] = newFilter;
1342 private void removeFilterFromArray(LogFilter oldFilter) {
1343 // look for the index
1345 for (int i = 0 ; i < mFilters.length ; i++) {
1346 if (mFilters[i] == oldFilter) {
1353 LogFilter[] newFilters = new LogFilter[mFilters.length-1];
1354 System.arraycopy(mFilters, 0, newFilters, 0, index);
1355 System.arraycopy(mFilters, index + 1, newFilters, index,
1356 newFilters.length-index);
1357 mFilters = newFilters;
1362 * Initialize the filter with already existing buffer.
1365 private void initFilter(LogFilter filter) {
1367 if (filter.uiReady() == false) {
1371 if (filter == mDefaultFilter) {
1372 initDefaultFilter();
1378 if (mBufferStart != -1) {
1379 int max = mBufferEnd;
1380 if (mBufferEnd < mBufferStart) {
1381 max += STRING_BUFFER_LENGTH;
1384 for (int i = mBufferStart; i < max; i++) {
1385 int realItemIndex = i % STRING_BUFFER_LENGTH;
1387 filter.addMessage(mBuffer[realItemIndex], null /* old message */);
1392 filter.resetTempFilteringStatus();
1396 * Refill the default filter. Not to be called directly.
1399 private void initDefaultFilter() {
1400 mDefaultFilter.clear();
1402 if (mBufferStart != -1) {
1403 int max = mBufferEnd;
1404 if (mBufferEnd < mBufferStart) {
1405 max += STRING_BUFFER_LENGTH;
1408 for (int i = mBufferStart; i < max; i++) {
1409 int realItemIndex = i % STRING_BUFFER_LENGTH;
1410 LogMessage msg = mBuffer[realItemIndex];
1412 // first we check that the other filters don't take this message
1413 boolean filtered = false;
1414 for (LogFilter f : mFilters) {
1415 filtered |= f.accept(msg);
1418 if (filtered == false) {
1419 mDefaultFilter.addMessage(msg, null /* old message */);
1424 mDefaultFilter.flush();
1425 mDefaultFilter.resetTempFilteringStatus();
1429 * Reset the filters, to handle change in device in automatic filter mode
1431 private void resetFilters() {
1432 // if we are in automatic mode, then we need to rmove the current
1434 if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
1437 // recreate the filters.
1443 private LogFilter getCurrentFilter() {
1444 int index = mFolders.getSelectionIndex();
1446 // if mFilters is null or index is invalid, we return the default
1447 // filter. It doesn't matter if that one is null as well, since we
1448 // would return null anyway.
1449 if (index == 0 || mFilters == null) {
1450 return mDefaultFilter;
1453 return mFilters[index-1];
1457 private void emptyTables() {
1458 for (LogFilter f : mFilters) {
1459 f.getTable().removeAll();
1462 if (mDefaultFilter != null) {
1463 mDefaultFilter.getTable().removeAll();
1467 protected void updateFilteringWith(String text) {
1468 synchronized (mBuffer) {
1469 // reset the temp filtering for all the filters
1470 for (LogFilter f : mFilters) {
1471 f.resetTempFiltering();
1473 if (mDefaultFilter != null) {
1474 mDefaultFilter.resetTempFiltering();
1477 // now we need to figure out the new temp filtering
1479 String[] segments = text.split(" "); //$NON-NLS-1$
1481 ArrayList<String> keywords = new ArrayList<String>(segments.length);
1483 // loop and look for temp id/tag
1485 String tempTag = null;
1486 for (int i = 0 ; i < segments.length; i++) {
1487 String s = segments[i];
1488 if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$
1490 String[] seg = s.split(":"); //$NON-NLS-1$
1491 if (seg.length == 2) {
1492 if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$
1493 tempPid = Integer.valueOf(seg[1]);
1496 } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$
1497 String seg[] = segments[i].split(":"); //$NON-NLS-1$
1498 if (seg.length == 2) {
1506 // set the temp filtering in the filters
1507 if (tempPid != -1 || tempTag != null || keywords.size() > 0) {
1508 String[] keywordsArray = keywords.toArray(
1509 new String[keywords.size()]);
1511 for (LogFilter f : mFilters) {
1512 if (tempPid != -1) {
1513 f.setTempPidFiltering(tempPid);
1515 if (tempTag != null) {
1516 f.setTempTagFiltering(tempTag);
1518 f.setTempKeywordFiltering(keywordsArray);
1521 if (mDefaultFilter != null) {
1522 if (tempPid != -1) {
1523 mDefaultFilter.setTempPidFiltering(tempPid);
1525 if (tempTag != null) {
1526 mDefaultFilter.setTempTagFiltering(tempTag);
1528 mDefaultFilter.setTempKeywordFiltering(keywordsArray);
1533 initFilter(mCurrentFilter);
1538 * Called when the current filter selection changes.
1539 * @param selectedFilter
1541 private void selectionChanged(LogFilter selectedFilter) {
1542 if (mLogLevelActions != null) {
1543 // get the log level
1544 int level = selectedFilter.getLogLevel();
1545 for (int i = 0 ; i < mLogLevelActions.length; i++) {
1546 ICommonAction a = mLogLevelActions[i];
1547 if (i == level - 2) {
1550 a.setChecked(false);
1555 if (mDeleteFilterAction != null) {
1556 mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete());
1558 if (mEditFilterAction != null) {
1559 mEditFilterAction.setEnabled(selectedFilter.supportsEdit());
1563 public String getSelectedErrorLineMessage() {
1564 Table table = mCurrentFilter.getTable();
1565 int[] selection = table.getSelectionIndices();
1567 if (selection.length == 1) {
1568 TableItem item = table.getItem(selection[0]);
1569 LogMessage msg = (LogMessage)item.getData();
1570 if (msg.data.logLevel == LogLevel.ERROR || msg.data.logLevel == LogLevel.WARN)