2 * Copyright (C) 2008 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.log.event;
19 import com.android.ddmlib.Client;
20 import com.android.ddmlib.IDevice;
21 import com.android.ddmlib.Log;
22 import com.android.ddmlib.Log.LogLevel;
23 import com.android.ddmlib.log.EventContainer;
24 import com.android.ddmlib.log.EventLogParser;
25 import com.android.ddmlib.log.LogReceiver;
26 import com.android.ddmlib.log.LogReceiver.ILogListener;
27 import com.android.ddmlib.log.LogReceiver.LogEntry;
28 import com.android.ddmuilib.DdmUiPreferences;
29 import com.android.ddmuilib.TablePanel;
30 import com.android.ddmuilib.actions.ICommonAction;
31 import com.android.ddmuilib.annotation.UiThread;
32 import com.android.ddmuilib.annotation.WorkerThread;
33 import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener;
35 import org.eclipse.jface.preference.IPreferenceStore;
36 import org.eclipse.swt.SWT;
37 import org.eclipse.swt.SWTException;
38 import org.eclipse.swt.custom.ScrolledComposite;
39 import org.eclipse.swt.events.ControlAdapter;
40 import org.eclipse.swt.events.ControlEvent;
41 import org.eclipse.swt.events.DisposeEvent;
42 import org.eclipse.swt.events.DisposeListener;
43 import org.eclipse.swt.graphics.Rectangle;
44 import org.eclipse.swt.layout.GridData;
45 import org.eclipse.swt.layout.RowData;
46 import org.eclipse.swt.layout.RowLayout;
47 import org.eclipse.swt.widgets.Composite;
48 import org.eclipse.swt.widgets.Control;
49 import org.eclipse.swt.widgets.Display;
50 import org.eclipse.swt.widgets.FileDialog;
51 import org.eclipse.swt.widgets.Table;
52 import org.eclipse.swt.widgets.TableColumn;
55 import java.io.FileInputStream;
56 import java.io.FileNotFoundException;
57 import java.io.FileOutputStream;
58 import java.io.IOException;
59 import java.text.NumberFormat;
60 import java.util.ArrayList;
61 import java.util.regex.Pattern;
66 public class EventLogPanel extends TablePanel implements ILogListener,
69 private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$
71 private final static String PREFS_EVENT_DISPLAY = "EventLogPanel.eventDisplay"; //$NON-NLS-1$
72 private final static String EVENT_DISPLAY_STORAGE_SEPARATOR = "|"; //$NON-NLS-1$
74 static final String PREFS_DISPLAY_WIDTH = "EventLogPanel.width"; //$NON-NLS-1$
75 static final String PREFS_DISPLAY_HEIGHT = "EventLogPanel.height"; //$NON-NLS-1$
77 private final static int DEFAULT_DISPLAY_WIDTH = 500;
78 private final static int DEFAULT_DISPLAY_HEIGHT = 400;
80 private IDevice mCurrentLoggedDevice;
81 private String mCurrentLogFile;
82 private LogReceiver mCurrentLogReceiver;
83 private EventLogParser mCurrentEventLogParser;
85 private Object mLock = new Object();
87 /** list of all the events. */
88 private final ArrayList<EventContainer> mEvents = new ArrayList<EventContainer>();
90 /** list of all the new events, that have yet to be displayed by the ui */
91 private final ArrayList<EventContainer> mNewEvents = new ArrayList<EventContainer>();
92 /** indicates a pending ui thread display */
93 private boolean mPendingDisplay = false;
95 /** list of all the custom event displays */
96 private final ArrayList<EventDisplay> mEventDisplays = new ArrayList<EventDisplay>();
98 private final NumberFormat mFormatter = NumberFormat.getInstance();
99 private Composite mParent;
100 private ScrolledComposite mBottomParentPanel;
101 private Composite mBottomPanel;
102 private ICommonAction mOptionsAction;
103 private ICommonAction mClearAction;
104 private ICommonAction mSaveAction;
105 private ICommonAction mLoadAction;
106 private ICommonAction mImportAction;
108 /** file containing the current log raw data. */
109 private File mTempFile = null;
111 public EventLogPanel() {
113 mFormatter.setGroupingUsed(true);
117 * Sets the external actions.
118 * <p/>This method sets up the {@link ICommonAction} objects to execute the proper code
119 * when triggered by using {@link ICommonAction#setRunnable(Runnable)}.
120 * <p/>It will also make sure they are enabled only when possible.
121 * @param optionsAction
125 * @param importAction
127 public void setActions(ICommonAction optionsAction, ICommonAction clearAction,
128 ICommonAction saveAction, ICommonAction loadAction, ICommonAction importAction) {
129 mOptionsAction = optionsAction;
130 mOptionsAction.setRunnable(new Runnable() {
136 mClearAction = clearAction;
137 mClearAction.setRunnable(new Runnable() {
143 mSaveAction = saveAction;
144 mSaveAction.setRunnable(new Runnable() {
147 FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
149 fileDialog.setText("Save Event Log");
150 fileDialog.setFileName("event.log");
152 String fileName = fileDialog.open();
153 if (fileName != null) {
156 } catch (IOException e1) {
161 mLoadAction = loadAction;
162 mLoadAction.setRunnable(new Runnable() {
164 FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
166 fileDialog.setText("Load Event Log");
168 String fileName = fileDialog.open();
169 if (fileName != null) {
175 mImportAction = importAction;
176 mImportAction.setRunnable(new Runnable() {
178 FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
180 fileDialog.setText("Import Bug Report");
182 String fileName = fileDialog.open();
183 if (fileName != null) {
184 importBugReport(fileName);
189 mOptionsAction.setEnabled(false);
190 mClearAction.setEnabled(false);
191 mSaveAction.setEnabled(false);
195 * Opens the option panel.
197 * <b>This must be called from the UI thread</b>
200 public void openOptionPanel() {
202 EventDisplayOptions dialog = new EventDisplayOptions(mParent.getShell());
203 if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) {
204 synchronized (mLock) {
205 // get the new EventDisplay list
206 mEventDisplays.clear();
207 mEventDisplays.addAll(dialog.getEventDisplays());
209 // since the list of EventDisplay changed, we store it.
215 } catch (SWTException e) {
216 Log.e("EventLog", e); //$NON-NLS-1$
223 * <b>This must be called from the UI thread</b>
225 public void clearLog() {
227 synchronized (mLock) {
230 mPendingDisplay = false;
231 for (EventDisplay eventDisplay : mEventDisplays) {
232 eventDisplay.resetUI();
235 } catch (SWTException e) {
236 Log.e("EventLog", e); //$NON-NLS-1$
241 * Saves the content of the event log into a file. The log is saved in the same
242 * binary format than on the device.
244 * @throws IOException
246 public void saveLog(String filePath) throws IOException {
247 if (mCurrentLoggedDevice != null && mCurrentEventLogParser != null) {
248 File destFile = new File(filePath);
249 destFile.createNewFile();
250 FileInputStream fis = new FileInputStream(mTempFile);
251 FileOutputStream fos = new FileOutputStream(destFile);
252 byte[] buffer = new byte[1024];
256 while ((count = fis.read(buffer)) != -1) {
257 fos.write(buffer, 0, count);
263 // now we save the tag file
264 filePath = filePath + TAG_FILE_EXT;
265 mCurrentEventLogParser.saveTags(filePath);
270 * Loads a binary event log (if has associated .tag file) or
271 * otherwise loads a textual event log.
272 * @param filePath Event log path (and base of potential tag file)
274 public void loadLog(String filePath) {
275 if ((new File(filePath + TAG_FILE_EXT)).exists()) {
276 startEventLogFromFiles(filePath);
279 EventLogImporter importer = new EventLogImporter(filePath);
280 String[] tags = importer.getTags();
281 String[] log = importer.getLog();
282 startEventLogFromContent(tags, log);
283 } catch (FileNotFoundException e) {
284 // If this fails, display the error message from startEventLogFromFiles,
285 // and pretend we never tried EventLogImporter
286 Log.logAndDisplay(Log.LogLevel.ERROR, "EventLog",
287 String.format("Failure to read %1$s", filePath + TAG_FILE_EXT));
293 public void importBugReport(String filePath) {
295 BugReportImporter importer = new BugReportImporter(filePath);
297 String[] tags = importer.getTags();
298 String[] log = importer.getLog();
300 startEventLogFromContent(tags, log);
302 } catch (FileNotFoundException e) {
303 Log.logAndDisplay(LogLevel.ERROR, "Import",
304 "Unable to import bug report: " + e.getMessage());
309 * @see com.android.ddmuilib.SelectionDependentPanel#clientSelected()
312 public void clientSelected() {
317 * @see com.android.ddmuilib.SelectionDependentPanel#deviceSelected()
320 public void deviceSelected() {
321 startEventLog(getCurrentDevice());
326 * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int)
328 public void clientChanged(Client client, int changeMask) {
333 * @see com.android.ddmuilib.Panel#createControl(org.eclipse.swt.widgets.Composite)
336 protected Control createControl(Composite parent) {
338 mParent.addDisposeListener(new DisposeListener() {
339 public void widgetDisposed(DisposeEvent e) {
340 synchronized (mLock) {
341 if (mCurrentLogReceiver != null) {
342 mCurrentLogReceiver.cancel();
343 mCurrentLogReceiver = null;
344 mCurrentEventLogParser = null;
345 mCurrentLoggedDevice = null;
346 mEventDisplays.clear();
353 final IPreferenceStore store = DdmUiPreferences.getStore();
355 // init some store stuff
356 store.setDefault(PREFS_DISPLAY_WIDTH, DEFAULT_DISPLAY_WIDTH);
357 store.setDefault(PREFS_DISPLAY_HEIGHT, DEFAULT_DISPLAY_HEIGHT);
359 mBottomParentPanel = new ScrolledComposite(parent, SWT.V_SCROLL);
360 mBottomParentPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
361 mBottomParentPanel.setExpandHorizontal(true);
362 mBottomParentPanel.setExpandVertical(true);
364 mBottomParentPanel.addControlListener(new ControlAdapter() {
366 public void controlResized(ControlEvent e) {
367 if (mBottomPanel != null) {
368 Rectangle r = mBottomParentPanel.getClientArea();
369 mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width,
377 // load the EventDisplay from storage.
383 return mBottomParentPanel;
387 * @see com.android.ddmuilib.Panel#postCreation()
390 protected void postCreation() {
395 * @see com.android.ddmuilib.Panel#setFocus()
398 public void setFocus() {
399 mBottomParentPanel.setFocus();
403 * Starts a new logcat and set mCurrentLogCat as the current receiver.
404 * @param device the device to connect logcat to.
406 private void startEventLog(final IDevice device) {
407 if (device == mCurrentLoggedDevice) {
411 // if we have a logcat already running
412 if (mCurrentLogReceiver != null) {
415 mCurrentLoggedDevice = null;
416 mCurrentLogFile = null;
418 if (device != null) {
419 // create a new output receiver
420 mCurrentLogReceiver = new LogReceiver(this);
422 // start the logcat in a different thread
423 new Thread("EventLog") { //$NON-NLS-1$
426 while (device.isOnline() == false &&
427 mCurrentLogReceiver != null &&
428 mCurrentLogReceiver.isCancelled() == false) {
431 } catch (InterruptedException e) {
436 if (mCurrentLogReceiver == null || mCurrentLogReceiver.isCancelled()) {
437 // logcat was stopped/cancelled before the device became ready.
442 mCurrentLoggedDevice = device;
443 synchronized (mLock) {
444 mCurrentEventLogParser = new EventLogParser();
445 mCurrentEventLogParser.init(device);
448 // update the event display with the new parser.
449 updateEventDisplays();
451 // prepare the temp file that will contain the raw data
452 mTempFile = File.createTempFile("android-event-", ".log");
454 device.runEventLogService(mCurrentLogReceiver);
455 } catch (Exception e) {
456 Log.e("EventLog", e);
464 private void startEventLogFromFiles(final String fileName) {
465 // if we have a logcat already running
466 if (mCurrentLogReceiver != null) {
469 mCurrentLoggedDevice = null;
470 mCurrentLogFile = null;
472 // create a new output receiver
473 mCurrentLogReceiver = new LogReceiver(this);
475 mSaveAction.setEnabled(false);
477 // start the logcat in a different thread
478 new Thread("EventLog") { //$NON-NLS-1$
482 mCurrentLogFile = fileName;
483 synchronized (mLock) {
484 mCurrentEventLogParser = new EventLogParser();
485 if (mCurrentEventLogParser.init(fileName + TAG_FILE_EXT) == false) {
486 mCurrentEventLogParser = null;
487 Log.logAndDisplay(LogLevel.ERROR, "EventLog",
488 String.format("Failure to read %1$s", fileName + TAG_FILE_EXT));
493 // update the event display with the new parser.
494 updateEventDisplays();
496 runLocalEventLogService(fileName, mCurrentLogReceiver);
497 } catch (Exception e) {
498 Log.e("EventLog", e);
505 private void startEventLogFromContent(final String[] tags, final String[] log) {
506 // if we have a logcat already running
507 if (mCurrentLogReceiver != null) {
510 mCurrentLoggedDevice = null;
511 mCurrentLogFile = null;
513 // create a new output receiver
514 mCurrentLogReceiver = new LogReceiver(this);
516 mSaveAction.setEnabled(false);
518 // start the logcat in a different thread
519 new Thread("EventLog") { //$NON-NLS-1$
523 synchronized (mLock) {
524 mCurrentEventLogParser = new EventLogParser();
525 if (mCurrentEventLogParser.init(tags) == false) {
526 mCurrentEventLogParser = null;
531 // update the event display with the new parser.
532 updateEventDisplays();
534 runLocalEventLogService(log, mCurrentLogReceiver);
535 } catch (Exception e) {
536 Log.e("EventLog", e);
544 public void stopEventLog(boolean inUiThread) {
545 if (mCurrentLogReceiver != null) {
546 mCurrentLogReceiver.cancel();
548 // when the thread finishes, no one will reference that object
549 // and it'll be destroyed
550 synchronized (mLock) {
551 mCurrentLogReceiver = null;
552 mCurrentEventLogParser = null;
554 mCurrentLoggedDevice = null;
557 mPendingDisplay = false;
563 if (mTempFile != null) {
569 private void resetUI(boolean inUiThread) {
572 // the ui is static we just empty it.
574 resetUiFromUiThread();
577 Display d = mBottomParentPanel.getDisplay();
579 // run sync as we need to update right now.
580 d.syncExec(new Runnable() {
582 if (mBottomParentPanel.isDisposed() == false) {
583 resetUiFromUiThread();
587 } catch (SWTException e) {
588 // display is disposed, we're quitting. Do nothing.
593 private void resetUiFromUiThread() {
594 synchronized(mLock) {
595 for (EventDisplay eventDisplay : mEventDisplays) {
596 eventDisplay.resetUI();
599 mOptionsAction.setEnabled(false);
600 mClearAction.setEnabled(false);
601 mSaveAction.setEnabled(false);
604 private void prepareDisplayUi() {
605 mBottomPanel = new Composite(mBottomParentPanel, SWT.NONE);
606 mBottomParentPanel.setContent(mBottomPanel);
609 private void createDisplayUi() {
610 RowLayout rowLayout = new RowLayout();
611 rowLayout.wrap = true;
612 rowLayout.pack = false;
613 rowLayout.justify = true;
614 rowLayout.fill = true;
615 rowLayout.type = SWT.HORIZONTAL;
616 mBottomPanel.setLayout(rowLayout);
618 IPreferenceStore store = DdmUiPreferences.getStore();
619 int displayWidth = store.getInt(PREFS_DISPLAY_WIDTH);
620 int displayHeight = store.getInt(PREFS_DISPLAY_HEIGHT);
622 for (EventDisplay eventDisplay : mEventDisplays) {
623 Control c = eventDisplay.createComposite(mBottomPanel, mCurrentEventLogParser, this);
625 RowData rd = new RowData();
626 rd.height = displayHeight;
627 rd.width = displayWidth;
631 Table table = eventDisplay.getTable();
633 addTableToFocusListener(table);
637 mBottomPanel.layout();
638 mBottomParentPanel.setMinSize(mBottomPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT));
639 mBottomParentPanel.layout();
643 * Rebuild the display ui.
646 private void rebuildUi() {
647 synchronized (mLock) {
648 // we need to rebuild the ui. First we get rid of it.
649 mBottomPanel.dispose();
657 boolean start_event = false;
658 synchronized (mNewEvents) {
659 mNewEvents.addAll(0, mEvents);
661 if (mPendingDisplay == false) {
662 mPendingDisplay = true;
668 scheduleUIEventHandler();
671 Rectangle r = mBottomParentPanel.getClientArea();
672 mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width,
679 * Processes a new {@link LogEntry} by parsing it with {@link EventLogParser} and displaying it.
680 * @param entry The new log entry
681 * @see LogReceiver.ILogListener#newEntry(LogEntry)
684 public void newEntry(LogEntry entry) {
685 synchronized (mLock) {
686 if (mCurrentEventLogParser != null) {
687 EventContainer event = mCurrentEventLogParser.parse(entry);
689 handleNewEvent(event);
696 private void handleNewEvent(EventContainer event) {
697 // add the event to the generic list
700 // add to the list of events that needs to be displayed, and trigger a
701 // new display if needed.
702 boolean start_event = false;
703 synchronized (mNewEvents) {
704 mNewEvents.add(event);
706 if (mPendingDisplay == false) {
707 mPendingDisplay = true;
712 if (start_event == false) {
717 scheduleUIEventHandler();
721 * Schedules the UI thread to execute a {@link Runnable} calling {@link #displayNewEvents()}.
723 private void scheduleUIEventHandler() {
725 Display d = mBottomParentPanel.getDisplay();
726 d.asyncExec(new Runnable() {
728 if (mBottomParentPanel.isDisposed() == false) {
729 if (mCurrentEventLogParser != null) {
735 } catch (SWTException e) {
736 // if the ui is disposed, do nothing
741 * Processes raw data coming from the log service.
742 * @see LogReceiver.ILogListener#newData(byte[], int, int)
744 public void newData(byte[] data, int offset, int length) {
745 if (mTempFile != null) {
747 FileOutputStream fos = new FileOutputStream(mTempFile, true /* append */);
748 fos.write(data, offset, length);
750 } catch (FileNotFoundException e) {
751 } catch (IOException e) {
757 private void displayNewEvents() {
758 // never display more than 1,000 events in this loop. We can't do too much in the UI thread.
761 // prepare the displays
762 for (EventDisplay eventDisplay : mEventDisplays) {
763 eventDisplay.startMultiEventDisplay();
766 // display the new events
767 EventContainer event = null;
768 boolean need_to_reloop = false;
770 // get the next event to display.
771 synchronized (mNewEvents) {
772 if (mNewEvents.size() > 0) {
774 // there are still events to be displayed, but we don't want to hog the
775 // UI thread for too long, so we stop this runnable, but launch a new
776 // one to keep going.
777 need_to_reloop = true;
780 event = mNewEvents.remove(0);
786 mPendingDisplay = false;
791 // notify the event display
792 for (EventDisplay eventDisplay : mEventDisplays) {
793 eventDisplay.newEvent(event, mCurrentEventLogParser);
796 } while (event != null);
798 // we're done displaying events.
799 for (EventDisplay eventDisplay : mEventDisplays) {
800 eventDisplay.endMultiEventDisplay();
803 // if needed, ask the UI thread to re-run this method.
804 if (need_to_reloop) {
805 scheduleUIEventHandler();
810 * Loads the {@link EventDisplay}s from the preference store.
812 private void loadEventDisplays() {
813 IPreferenceStore store = DdmUiPreferences.getStore();
814 String storage = store.getString(PREFS_EVENT_DISPLAY);
816 if (storage.length() > 0) {
817 String[] values = storage.split(Pattern.quote(EVENT_DISPLAY_STORAGE_SEPARATOR));
819 for (String value : values) {
820 EventDisplay eventDisplay = EventDisplay.load(value);
821 if (eventDisplay != null) {
822 mEventDisplays.add(eventDisplay);
829 * Saves the {@link EventDisplay}s into the {@link DdmUiPreferences} store.
831 private void saveEventDisplays() {
832 IPreferenceStore store = DdmUiPreferences.getStore();
834 boolean first = true;
835 StringBuilder sb = new StringBuilder();
837 for (EventDisplay eventDisplay : mEventDisplays) {
838 String storage = eventDisplay.getStorageString();
839 if (storage != null) {
840 if (first == false) {
841 sb.append(EVENT_DISPLAY_STORAGE_SEPARATOR);
850 store.setValue(PREFS_EVENT_DISPLAY, sb.toString());
854 * Updates the {@link EventDisplay} with the new {@link EventLogParser}.
856 * This will run asynchronously in the UI thread.
859 private void updateEventDisplays() {
861 Display d = mBottomParentPanel.getDisplay();
863 d.asyncExec(new Runnable() {
865 if (mBottomParentPanel.isDisposed() == false) {
866 for (EventDisplay eventDisplay : mEventDisplays) {
867 eventDisplay.setNewLogParser(mCurrentEventLogParser);
870 mOptionsAction.setEnabled(true);
871 mClearAction.setEnabled(true);
872 if (mCurrentLogFile == null) {
873 mSaveAction.setEnabled(true);
875 mSaveAction.setEnabled(false);
880 } catch (SWTException e) {
881 // display is disposed: do nothing.
886 public void columnResized(int index, TableColumn sourceColumn) {
887 for (EventDisplay eventDisplay : mEventDisplays) {
888 eventDisplay.resizeColumn(index, sourceColumn);
893 * Runs an event log service out of a local file.
894 * @param fileName the full file name of the local file containing the event log.
895 * @param logReceiver the receiver that will handle the log
896 * @throws IOException
899 private void runLocalEventLogService(String fileName, LogReceiver logReceiver)
901 byte[] buffer = new byte[256];
903 FileInputStream fis = new FileInputStream(fileName);
906 while ((count = fis.read(buffer)) != -1) {
907 logReceiver.parseNewData(buffer, 0, count);
912 private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) {
913 synchronized (mLock) {
914 for (String line : log) {
915 EventContainer event = mCurrentEventLogParser.parse(line);
917 handleNewEvent(event);