/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ddmuilib.log.event; import com.android.ddmlib.Client; import com.android.ddmlib.Device; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.LogReceiver; import com.android.ddmlib.log.LogReceiver.ILogListener; import com.android.ddmlib.log.LogReceiver.LogEntry; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.IImageLoader; import com.android.ddmuilib.TablePanel; import com.android.ddmuilib.actions.ICommonAction; import com.android.ddmuilib.annotation.UiThread; import com.android.ddmuilib.annotation.WorkerThread; import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.RowData; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.NumberFormat; import java.util.ArrayList; import java.util.regex.Pattern; /** * Event log viewer */ public class EventLogPanel extends TablePanel implements ILogListener, ILogColumnListener { private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$ private final static String PREFS_EVENT_DISPLAY = "EventLogPanel.eventDisplay"; //$NON-NLS-1$ private final static String EVENT_DISPLAY_STORAGE_SEPARATOR = "|"; //$NON-NLS-1$ static final String PREFS_DISPLAY_WIDTH = "EventLogPanel.width"; //$NON-NLS-1$ static final String PREFS_DISPLAY_HEIGHT = "EventLogPanel.height"; //$NON-NLS-1$ private final static int DEFAULT_DISPLAY_WIDTH = 500; private final static int DEFAULT_DISPLAY_HEIGHT = 400; private IImageLoader mImageLoader; private Device mCurrentLoggedDevice; private String mCurrentLogFile; private LogReceiver mCurrentLogReceiver; private EventLogParser mCurrentEventLogParser; private Object mLock = new Object(); /** list of all the events. */ private final ArrayList<EventContainer> mEvents = new ArrayList<EventContainer>(); /** list of all the new events, that have yet to be displayed by the ui */ private final ArrayList<EventContainer> mNewEvents = new ArrayList<EventContainer>(); /** indicates a pending ui thread display */ private boolean mPendingDisplay = false; /** list of all the custom event displays */ private final ArrayList<EventDisplay> mEventDisplays = new ArrayList<EventDisplay>(); private final NumberFormat mFormatter = NumberFormat.getInstance(); private Composite mParent; private ScrolledComposite mBottomParentPanel; private Composite mBottomPanel; private ICommonAction mOptionsAction; private ICommonAction mClearAction; private ICommonAction mSaveAction; private ICommonAction mLoadAction; private ICommonAction mImportAction; /** file containing the current log raw data. */ private File mTempFile = null; public EventLogPanel(IImageLoader imageLoader) { super(); mImageLoader = imageLoader; mFormatter.setGroupingUsed(true); } /** * Sets the external actions. * <p/>This method sets up the {@link ICommonAction} objects to execute the proper code * when triggered by using {@link ICommonAction#setRunnable(Runnable)}. * <p/>It will also make sure they are enabled only when possible. * @param optionsAction * @param clearAction * @param saveAction * @param loadAction * @param importAction */ public void setActions(ICommonAction optionsAction, ICommonAction clearAction, ICommonAction saveAction, ICommonAction loadAction, ICommonAction importAction) { mOptionsAction = optionsAction; mOptionsAction.setRunnable(new Runnable() { public void run() { openOptionPanel(); } }); mClearAction = clearAction; mClearAction.setRunnable(new Runnable() { public void run() { clearLog(); } }); mSaveAction = saveAction; mSaveAction.setRunnable(new Runnable() { public void run() { try { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); fileDialog.setText("Save Event Log"); fileDialog.setFileName("event.log"); String fileName = fileDialog.open(); if (fileName != null) { saveLog(fileName); } } catch (IOException e1) { } } }); mLoadAction = loadAction; mLoadAction.setRunnable(new Runnable() { public void run() { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); fileDialog.setText("Load Event Log"); String fileName = fileDialog.open(); if (fileName != null) { loadLog(fileName); } } }); mImportAction = importAction; mImportAction.setRunnable(new Runnable() { public void run() { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); fileDialog.setText("Import Bug Report"); String fileName = fileDialog.open(); if (fileName != null) { importBugReport(fileName); } } }); mOptionsAction.setEnabled(false); mClearAction.setEnabled(false); mSaveAction.setEnabled(false); } /** * Opens the option panel. * </p> * <b>This must be called from the UI thread</b> */ @UiThread public void openOptionPanel() { try { EventDisplayOptions dialog = new EventDisplayOptions(mImageLoader, mParent.getShell()); if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) { synchronized (mLock) { // get the new EventDisplay list mEventDisplays.clear(); mEventDisplays.addAll(dialog.getEventDisplays()); // since the list of EventDisplay changed, we store it. saveEventDisplays(); rebuildUi(); } } } catch (SWTException e) { Log.e("EventLog", e); //$NON-NLS-1$ } } /** * Clears the log. * <p/> * <b>This must be called from the UI thread</b> */ public void clearLog() { try { synchronized (mLock) { mEvents.clear(); mNewEvents.clear(); mPendingDisplay = false; for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.resetUI(); } } } catch (SWTException e) { Log.e("EventLog", e); //$NON-NLS-1$ } } /** * Saves the content of the event log into a file. The log is saved in the same * binary format than on the device. * @param filePath * @throws IOException */ public void saveLog(String filePath) throws IOException { if (mCurrentLoggedDevice != null && mCurrentEventLogParser != null) { File destFile = new File(filePath); destFile.createNewFile(); FileInputStream fis = new FileInputStream(mTempFile); FileOutputStream fos = new FileOutputStream(destFile); byte[] buffer = new byte[1024]; int count; while ((count = fis.read(buffer)) != -1) { fos.write(buffer, 0, count); } fos.close(); fis.close(); // now we save the tag file filePath = filePath + TAG_FILE_EXT; mCurrentEventLogParser.saveTags(filePath); } } /** * Loads a binary event log (if has associated .tag file) or * otherwise loads a textual event log. * @param filePath Event log path (and base of potential tag file) */ public void loadLog(String filePath) { if ((new File(filePath + TAG_FILE_EXT)).exists()) { startEventLogFromFiles(filePath); } else { try { EventLogImporter importer = new EventLogImporter(filePath); String[] tags = importer.getTags(); String[] log = importer.getLog(); startEventLogFromContent(tags, log); } catch (FileNotFoundException e) { // If this fails, display the error message from startEventLogFromFiles, // and pretend we never tried EventLogImporter Log.logAndDisplay(Log.LogLevel.ERROR, "EventLog", String.format("Failure to read %1$s", filePath + TAG_FILE_EXT)); } } } public void importBugReport(String filePath) { try { BugReportImporter importer = new BugReportImporter(filePath); String[] tags = importer.getTags(); String[] log = importer.getLog(); startEventLogFromContent(tags, log); } catch (FileNotFoundException e) { Log.logAndDisplay(LogLevel.ERROR, "Import", "Unable to import bug report: " + e.getMessage()); } } /* (non-Javadoc) * @see com.android.ddmuilib.SelectionDependentPanel#clientSelected() */ @Override public void clientSelected() { // pass } /* (non-Javadoc) * @see com.android.ddmuilib.SelectionDependentPanel#deviceSelected() */ @Override public void deviceSelected() { startEventLog(getCurrentDevice()); } /* * (non-Javadoc) * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int) */ public void clientChanged(Client client, int changeMask) { // pass } /* (non-Javadoc) * @see com.android.ddmuilib.Panel#createControl(org.eclipse.swt.widgets.Composite) */ @Override protected Control createControl(Composite parent) { mParent = parent; mParent.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { synchronized (mLock) { if (mCurrentLogReceiver != null) { mCurrentLogReceiver.cancel(); mCurrentLogReceiver = null; mCurrentEventLogParser = null; mCurrentLoggedDevice = null; mEventDisplays.clear(); mEvents.clear(); } } } }); final IPreferenceStore store = DdmUiPreferences.getStore(); // init some store stuff store.setDefault(PREFS_DISPLAY_WIDTH, DEFAULT_DISPLAY_WIDTH); store.setDefault(PREFS_DISPLAY_HEIGHT, DEFAULT_DISPLAY_HEIGHT); mBottomParentPanel = new ScrolledComposite(parent, SWT.V_SCROLL); mBottomParentPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); mBottomParentPanel.setExpandHorizontal(true); mBottomParentPanel.setExpandVertical(true); mBottomParentPanel.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { if (mBottomPanel != null) { Rectangle r = mBottomParentPanel.getClientArea(); mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width, SWT.DEFAULT)); } } }); prepareDisplayUi(); // load the EventDisplay from storage. loadEventDisplays(); // create the ui createDisplayUi(); return mBottomParentPanel; } /* (non-Javadoc) * @see com.android.ddmuilib.Panel#postCreation() */ @Override protected void postCreation() { // pass } /* (non-Javadoc) * @see com.android.ddmuilib.Panel#setFocus() */ @Override public void setFocus() { mBottomParentPanel.setFocus(); } /** * Starts a new logcat and set mCurrentLogCat as the current receiver. * @param device the device to connect logcat to. */ private void startEventLog(final Device device) { if (device == mCurrentLoggedDevice) { return; } // if we have a logcat already running if (mCurrentLogReceiver != null) { stopEventLog(false); } mCurrentLoggedDevice = null; mCurrentLogFile = null; if (device != null) { // create a new output receiver mCurrentLogReceiver = new LogReceiver(this); // start the logcat in a different thread new Thread("EventLog") { //$NON-NLS-1$ @Override public void run() { while (device.isOnline() == false && mCurrentLogReceiver != null && mCurrentLogReceiver.isCancelled() == false) { try { sleep(2000); } catch (InterruptedException e) { return; } } if (mCurrentLogReceiver == null || mCurrentLogReceiver.isCancelled()) { // logcat was stopped/cancelled before the device became ready. return; } try { mCurrentLoggedDevice = device; synchronized (mLock) { mCurrentEventLogParser = new EventLogParser(); mCurrentEventLogParser.init(device); } // update the event display with the new parser. updateEventDisplays(); // prepare the temp file that will contain the raw data mTempFile = File.createTempFile("android-event-", ".log"); device.runEventLogService(mCurrentLogReceiver); } catch (Exception e) { Log.e("EventLog", e); } finally { } } }.start(); } } private void startEventLogFromFiles(final String fileName) { // if we have a logcat already running if (mCurrentLogReceiver != null) { stopEventLog(false); } mCurrentLoggedDevice = null; mCurrentLogFile = null; // create a new output receiver mCurrentLogReceiver = new LogReceiver(this); mSaveAction.setEnabled(false); // start the logcat in a different thread new Thread("EventLog") { //$NON-NLS-1$ @Override public void run() { try { mCurrentLogFile = fileName; synchronized (mLock) { mCurrentEventLogParser = new EventLogParser(); if (mCurrentEventLogParser.init(fileName + TAG_FILE_EXT) == false) { mCurrentEventLogParser = null; Log.logAndDisplay(LogLevel.ERROR, "EventLog", String.format("Failure to read %1$s", fileName + TAG_FILE_EXT)); return; } } // update the event display with the new parser. updateEventDisplays(); runLocalEventLogService(fileName, mCurrentLogReceiver); } catch (Exception e) { Log.e("EventLog", e); } finally { } } }.start(); } private void startEventLogFromContent(final String[] tags, final String[] log) { // if we have a logcat already running if (mCurrentLogReceiver != null) { stopEventLog(false); } mCurrentLoggedDevice = null; mCurrentLogFile = null; // create a new output receiver mCurrentLogReceiver = new LogReceiver(this); mSaveAction.setEnabled(false); // start the logcat in a different thread new Thread("EventLog") { //$NON-NLS-1$ @Override public void run() { try { synchronized (mLock) { mCurrentEventLogParser = new EventLogParser(); if (mCurrentEventLogParser.init(tags) == false) { mCurrentEventLogParser = null; return; } } // update the event display with the new parser. updateEventDisplays(); runLocalEventLogService(log, mCurrentLogReceiver); } catch (Exception e) { Log.e("EventLog", e); } finally { } } }.start(); } public void stopEventLog(boolean inUiThread) { if (mCurrentLogReceiver != null) { mCurrentLogReceiver.cancel(); // when the thread finishes, no one will reference that object // and it'll be destroyed synchronized (mLock) { mCurrentLogReceiver = null; mCurrentEventLogParser = null; mCurrentLoggedDevice = null; mEvents.clear(); mNewEvents.clear(); mPendingDisplay = false; } resetUI(inUiThread); } if (mTempFile != null) { mTempFile.delete(); mTempFile = null; } } private void resetUI(boolean inUiThread) { mEvents.clear(); // the ui is static we just empty it. if (inUiThread) { resetUiFromUiThread(); } else { try { Display d = mBottomParentPanel.getDisplay(); // run sync as we need to update right now. d.syncExec(new Runnable() { public void run() { if (mBottomParentPanel.isDisposed() == false) { resetUiFromUiThread(); } } }); } catch (SWTException e) { // display is disposed, we're quitting. Do nothing. } } } private void resetUiFromUiThread() { synchronized(mLock) { for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.resetUI(); } } mOptionsAction.setEnabled(false); mClearAction.setEnabled(false); mSaveAction.setEnabled(false); } private void prepareDisplayUi() { mBottomPanel = new Composite(mBottomParentPanel, SWT.NONE); mBottomParentPanel.setContent(mBottomPanel); } private void createDisplayUi() { RowLayout rowLayout = new RowLayout(); rowLayout.wrap = true; rowLayout.pack = false; rowLayout.justify = true; rowLayout.fill = true; rowLayout.type = SWT.HORIZONTAL; mBottomPanel.setLayout(rowLayout); IPreferenceStore store = DdmUiPreferences.getStore(); int displayWidth = store.getInt(PREFS_DISPLAY_WIDTH); int displayHeight = store.getInt(PREFS_DISPLAY_HEIGHT); for (EventDisplay eventDisplay : mEventDisplays) { Control c = eventDisplay.createComposite(mBottomPanel, mCurrentEventLogParser, this); if (c != null) { RowData rd = new RowData(); rd.height = displayHeight; rd.width = displayWidth; c.setLayoutData(rd); } Table table = eventDisplay.getTable(); if (table != null) { addTableToFocusListener(table); } } mBottomPanel.layout(); mBottomParentPanel.setMinSize(mBottomPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT)); mBottomParentPanel.layout(); } /** * Rebuild the display ui. */ @UiThread private void rebuildUi() { synchronized (mLock) { // we need to rebuild the ui. First we get rid of it. mBottomPanel.dispose(); mBottomPanel = null; prepareDisplayUi(); createDisplayUi(); // and fill it boolean start_event = false; synchronized (mNewEvents) { mNewEvents.addAll(0, mEvents); if (mPendingDisplay == false) { mPendingDisplay = true; start_event = true; } } if (start_event) { scheduleUIEventHandler(); } Rectangle r = mBottomParentPanel.getClientArea(); mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width, SWT.DEFAULT)); } } /** * Processes a new {@link LogEntry} by parsing it with {@link EventLogParser} and displaying it. * @param entry The new log entry * @see LogReceiver.ILogListener#newEntry(LogEntry) */ @WorkerThread public void newEntry(LogEntry entry) { synchronized (mLock) { if (mCurrentEventLogParser != null) { EventContainer event = mCurrentEventLogParser.parse(entry); if (event != null) { handleNewEvent(event); } } } } @WorkerThread private void handleNewEvent(EventContainer event) { // add the event to the generic list mEvents.add(event); // add to the list of events that needs to be displayed, and trigger a // new display if needed. boolean start_event = false; synchronized (mNewEvents) { mNewEvents.add(event); if (mPendingDisplay == false) { mPendingDisplay = true; start_event = true; } } if (start_event == false) { // we're done return; } scheduleUIEventHandler(); } /** * Schedules the UI thread to execute a {@link Runnable} calling {@link #displayNewEvents()}. */ private void scheduleUIEventHandler() { try { Display d = mBottomParentPanel.getDisplay(); d.asyncExec(new Runnable() { public void run() { if (mBottomParentPanel.isDisposed() == false) { if (mCurrentEventLogParser != null) { displayNewEvents(); } } } }); } catch (SWTException e) { // if the ui is disposed, do nothing } } /** * Processes raw data coming from the log service. * @see LogReceiver.ILogListener#newData(byte[], int, int) */ public void newData(byte[] data, int offset, int length) { if (mTempFile != null) { try { FileOutputStream fos = new FileOutputStream(mTempFile, true /* append */); fos.write(data, offset, length); fos.close(); } catch (FileNotFoundException e) { } catch (IOException e) { } } } @UiThread private void displayNewEvents() { // never display more than 1,000 events in this loop. We can't do too much in the UI thread. int count = 0; // prepare the displays for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.startMultiEventDisplay(); } // display the new events EventContainer event = null; boolean need_to_reloop = false; do { // get the next event to display. synchronized (mNewEvents) { if (mNewEvents.size() > 0) { if (count > 200) { // there are still events to be displayed, but we don't want to hog the // UI thread for too long, so we stop this runnable, but launch a new // one to keep going. need_to_reloop = true; event = null; } else { event = mNewEvents.remove(0); count++; } } else { // we're done. event = null; mPendingDisplay = false; } } if (event != null) { // notify the event display for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.newEvent(event, mCurrentEventLogParser); } } } while (event != null); // we're done displaying events. for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.endMultiEventDisplay(); } // if needed, ask the UI thread to re-run this method. if (need_to_reloop) { scheduleUIEventHandler(); } } /** * Loads the {@link EventDisplay}s from the preference store. */ private void loadEventDisplays() { IPreferenceStore store = DdmUiPreferences.getStore(); String storage = store.getString(PREFS_EVENT_DISPLAY); if (storage.length() > 0) { String[] values = storage.split(Pattern.quote(EVENT_DISPLAY_STORAGE_SEPARATOR)); for (String value : values) { EventDisplay eventDisplay = EventDisplay.load(value); if (eventDisplay != null) { mEventDisplays.add(eventDisplay); } } } } /** * Saves the {@link EventDisplay}s into the {@link DdmUiPreferences} store. */ private void saveEventDisplays() { IPreferenceStore store = DdmUiPreferences.getStore(); boolean first = true; StringBuilder sb = new StringBuilder(); for (EventDisplay eventDisplay : mEventDisplays) { String storage = eventDisplay.getStorageString(); if (storage != null) { if (first == false) { sb.append(EVENT_DISPLAY_STORAGE_SEPARATOR); } else { first = false; } sb.append(storage); } } store.setValue(PREFS_EVENT_DISPLAY, sb.toString()); } /** * Updates the {@link EventDisplay} with the new {@link EventLogParser}. * <p/> * This will run asynchronously in the UI thread. */ @WorkerThread private void updateEventDisplays() { try { Display d = mBottomParentPanel.getDisplay(); d.asyncExec(new Runnable() { public void run() { if (mBottomParentPanel.isDisposed() == false) { for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.setNewLogParser(mCurrentEventLogParser); } mOptionsAction.setEnabled(true); mClearAction.setEnabled(true); if (mCurrentLogFile == null) { mSaveAction.setEnabled(true); } else { mSaveAction.setEnabled(false); } } } }); } catch (SWTException e) { // display is disposed: do nothing. } } @UiThread public void columnResized(int index, TableColumn sourceColumn) { for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.resizeColumn(index, sourceColumn); } } /** * Runs an event log service out of a local file. * @param fileName the full file name of the local file containing the event log. * @param logReceiver the receiver that will handle the log * @throws IOException */ @WorkerThread private void runLocalEventLogService(String fileName, LogReceiver logReceiver) throws IOException { byte[] buffer = new byte[256]; FileInputStream fis = new FileInputStream(fileName); int count; while ((count = fis.read(buffer)) != -1) { logReceiver.parseNewData(buffer, 0, count); } } @WorkerThread private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) { synchronized (mLock) { for (String line : log) { EventContainer event = mCurrentEventLogParser.parse(line); if (event != null) { handleNewEvent(event); } } } } }