FileDocCategorySizeDatePackage
EventLogPanel.javaAPI DocAndroid 1.5 API31382Wed May 06 22:41:08 BST 2009com.android.ddmuilib.log.event

EventLogPanel.java

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