/* * Copyright 2000-2012 JetBrains s.r.o. * * 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 org.jetbrains.android.logcat; import com.android.ddmlib.Client; import com.android.ddmlib.IDevice; import com.android.tools.idea.ddms.DeviceContext; import com.intellij.diagnostic.logging.LogConsoleBase; import com.intellij.diagnostic.logging.LogConsoleListener; import com.intellij.execution.impl.ConsoleViewImpl; import com.intellij.execution.ui.ConsoleView; import com.intellij.icons.AllIcons; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import icons.AndroidIcons; import org.jetbrains.android.dom.manifest.Application; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.List; import static javax.swing.BoxLayout.X_AXIS; /** * @author Eugene.Kudelevsky */ public abstract class AndroidLogcatView implements Disposable { private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.logcat.AndroidLogcatView"); public static final Key<AndroidLogcatView> ANDROID_LOGCAT_VIEW_KEY = Key.create("ANDROID_LOGCAT_VIEW_KEY"); public static final String NO_FILTERS = AndroidBundle.message("android.logcat.filters.none"); public static final String EDIT_FILTER_CONFIGURATION = AndroidBundle.message("android.logcat.filters.edit"); /** * The {@link #FILTER_LOGCAT_WHEN_SELECTION_CHANGES} property controls whether selecting a VM in the device panel automatically * filters logcat to only show messages from that client only. */ @NonNls private static final String FILTER_LOGCAT_WHEN_SELECTION_CHANGES = "android.logcat.filter.apply.if.client.selection.changes"; private final Project myProject; private final DeviceContext myDeviceContext; private JPanel myPanel; private DefaultComboBoxModel myFilterComboBoxModel; private String myCurrentFilterName; private volatile IDevice myDevice; private final Object myLock = new Object(); private final LogConsoleBase myLogConsole; private final AndroidLogFilterModel myLogFilterModel; private volatile Reader myCurrentReader; private volatile Writer myCurrentWriter; private final IDevice myPreselectedDevice; private void updateInUIThread() { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (myProject.isDisposed()) { return; } updateLogConsole(); } }); } Project getProject() { return myProject; } @NotNull public LogConsoleBase getLogConsole() { return myLogConsole; } private class MyLoggingReader extends AndroidLoggingReader { @Override @NotNull protected Object getLock() { return myLock; } @Override protected Reader getReader() { return myCurrentReader; } } /** * Logcat view for provided device */ public AndroidLogcatView(@NotNull final Project project, @NotNull IDevice preselectedDevice) { this(project, preselectedDevice, null); } /** * Logcat view with device obtained from {@link DeviceContext} */ public AndroidLogcatView(@NotNull final Project project, @NotNull DeviceContext deviceContext) { this(project, null, deviceContext); } @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) private AndroidLogcatView(final Project project, @Nullable IDevice preselectedDevice, @Nullable DeviceContext deviceContext) { myDeviceContext = deviceContext; myProject = project; myPreselectedDevice = preselectedDevice; Disposer.register(myProject, this); myLogFilterModel = new AndroidLogFilterModel() { @Nullable private ConfiguredFilter myConfiguredFilter; @Override protected void setCustomFilter(String filter) { AndroidLogcatFiltersPreferences.getInstance(project).TOOL_WINDOW_CUSTOM_FILTER = filter; } @Override protected void saveLogLevel(String logLevelName) { AndroidLogcatFiltersPreferences.getInstance(project).TOOL_WINDOW_LOG_LEVEL = logLevelName; } @Override public String getSelectedLogLevelName() { return AndroidLogcatFiltersPreferences.getInstance(project).TOOL_WINDOW_LOG_LEVEL; } @Override public String getCustomFilter() { return AndroidLogcatFiltersPreferences.getInstance(project).TOOL_WINDOW_CUSTOM_FILTER; } @Override protected void setConfiguredFilter(@Nullable ConfiguredFilter filter) { AndroidLogcatFiltersPreferences.getInstance(project).TOOL_WINDOW_CONFIGURED_FILTER = filter != null ? filter.getName() : ""; myConfiguredFilter = filter; } @Nullable @Override protected ConfiguredFilter getConfiguredFilter() { if (myConfiguredFilter == null) { final String name = AndroidLogcatFiltersPreferences.getInstance(project).TOOL_WINDOW_CONFIGURED_FILTER; myConfiguredFilter = compileConfiguredFilter(name); } return myConfiguredFilter; } }; myLogConsole = new AndroidLogConsole(project, myLogFilterModel); myLogConsole.addListener(new LogConsoleListener() { @Override public void loggingWillBeStopped() { if (myCurrentWriter != null) { try { myCurrentWriter.close(); } catch (IOException e) { LOG.error(e); } } } }); if (preselectedDevice == null && deviceContext != null) { DeviceContext.DeviceSelectionListener deviceSelectionListener = new DeviceContext.DeviceSelectionListener() { @Override public void deviceSelected(@Nullable IDevice device) { updateInUIThread(); } @Override public void deviceChanged(@NotNull IDevice device, int changeMask) { if (device == myDevice && ((changeMask & IDevice.CHANGE_STATE) == IDevice.CHANGE_STATE)) { myDevice = null; updateInUIThread(); } } @Override public void clientSelected(@Nullable final Client c) { if (PropertiesComponent.getInstance().getBoolean(FILTER_LOGCAT_WHEN_SELECTION_CHANGES, true)) { if (ApplicationManager.getApplication().isDispatchThread()) { createAndSelectFilterForClient(c); } else { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { createAndSelectFilterForClient(c); } }); } } } }; deviceContext.addListener(deviceSelectionListener, this); } JComponent consoleComponent = myLogConsole.getComponent(); final ConsoleView console = myLogConsole.getConsole(); if (console != null) { final ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, myLogConsole.getOrCreateActions(), false); toolbar.setTargetComponent(console.getComponent()); final JComponent tbComp1 = toolbar.getComponent(); myPanel.add(tbComp1, BorderLayout.WEST); } myPanel.add(consoleComponent, BorderLayout.CENTER); Disposer.register(this, myLogConsole); updateLogConsole(); selectFilter(AndroidLogcatFiltersPreferences.getInstance(myProject).TOOL_WINDOW_CONFIGURED_FILTER); } @NotNull public ActionGroup getToolbarActions() { DefaultActionGroup group = new DefaultActionGroup(); group.add(new MyFilterSelectedClientAction()); return group; } @NotNull public JPanel createSearchComponent(final Project project) { final JPanel panel = new JPanel(); final JComboBox editFiltersCombo = new JComboBox(); myFilterComboBoxModel = new DefaultComboBoxModel(); editFiltersCombo.setModel(myFilterComboBoxModel); final String configuredFilter = AndroidLogcatFiltersPreferences. getInstance(myProject).TOOL_WINDOW_CONFIGURED_FILTER; updateConfiguredFilters(configuredFilter != null && configuredFilter.length() > 0 ? configuredFilter : NO_FILTERS); // note: the listener is added after the initial call to populate the combo // boxes in the above call to updateConfiguredFilters editFiltersCombo.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String prev = myCurrentFilterName; myCurrentFilterName = (String)editFiltersCombo.getSelectedItem(); if (myCurrentFilterName == null || myCurrentFilterName.equals(prev)) { return; } if (EDIT_FILTER_CONFIGURATION.equals(myCurrentFilterName)) { final EditLogFilterDialog dialog = new EditLogFilterDialog(AndroidLogcatView.this, prev); dialog.setTitle(AndroidBundle.message("android.logcat.new.filter.dialog.title")); dialog.show(); if (dialog.isOK()) { final AndroidConfiguredLogFilters.MyFilterEntry newEntry = dialog.getCustomLogFiltersEntry(); updateConfiguredFilters(newEntry != null ? newEntry.getName() : NO_FILTERS); } else { myCurrentFilterName = prev; editFiltersCombo.setSelectedItem(myCurrentFilterName); } } else { selectFilter(myCurrentFilterName); } // If users explicitly select a filter they want to use, then disable auto filter PropertiesComponent.getInstance().setValue(FILTER_LOGCAT_WHEN_SELECTION_CHANGES, Boolean.FALSE.toString()); } }); panel.add(editFiltersCombo); final JPanel searchComponent = new JPanel(); searchComponent.setLayout(new BoxLayout(searchComponent, X_AXIS)); searchComponent.add(myLogConsole.getSearchComponent()); searchComponent.add(panel); return searchComponent; } protected abstract boolean isActive(); public void activate() { if (isActive()) { updateLogConsole(); selectFilter(AndroidLogcatFiltersPreferences.getInstance(myProject).TOOL_WINDOW_CONFIGURED_FILTER); } if (myLogConsole != null) { myLogConsole.activate(); } } private void updateLogConsole() { IDevice device = getSelectedDevice(); if (myDevice != device) { synchronized (myLock) { myDevice = device; if (myCurrentWriter != null) { try { myCurrentWriter.close(); } catch (IOException e) { LOG.error(e); } } if (myCurrentReader != null) { try { myCurrentReader.close(); } catch (IOException e) { LOG.error(e); } } if (device != null) { final ConsoleView console = myLogConsole.getConsole(); if (console != null) { console.clear(); } final Pair<Reader, Writer> pair = AndroidLogcatUtil.startLoggingThread(myProject, device, false, myLogConsole); if (pair != null) { myCurrentReader = pair.first; myCurrentWriter = pair.second; } else { myCurrentReader = null; myCurrentWriter = null; } } } } } @Nullable public IDevice getSelectedDevice() { if (myPreselectedDevice != null) { return myPreselectedDevice; } else if (myDeviceContext != null) { return myDeviceContext.getSelectedDevice(); } else { return null; } } @Nullable private ConfiguredFilter compileConfiguredFilter(@NotNull String name) { if (NO_FILTERS.equals(name)) { return null; } final AndroidConfiguredLogFilters.MyFilterEntry entry = AndroidConfiguredLogFilters.getInstance(myProject).findFilterEntryByName(name); return ConfiguredFilter.compile(entry, name); } private void selectFilter(@NotNull final String filterName) { final ConfiguredFilter filter = compileConfiguredFilter(filterName); selectFilter(filter, filterName); } private void selectFilter(@Nullable final ConfiguredFilter filter, @NotNull final String filterName) { ProgressManager.getInstance().run( new Task.Backgroundable(myProject, LogConsoleBase.APPLYING_FILTER_TITLE) { @Override public void run(@NotNull ProgressIndicator indicator) { myLogFilterModel.updateConfiguredFilter(filter); myCurrentFilterName = filterName; } }); } private void createAndSelectFilterForClient(@Nullable Client client) { ConfiguredFilter filter; String name; if (client != null) { AndroidConfiguredLogFilters.MyFilterEntry f = AndroidConfiguredLogFilters.getInstance(myProject).createFilterForProcess(client.getClientData().getPid()); name = f.getName(); filter = ConfiguredFilter.compile(f, name); } else { filter = null; name = NO_FILTERS; } selectFilter(filter, name); myFilterComboBoxModel.setSelectedItem(NO_FILTERS.equals(name) ? NO_FILTERS : null); } /** Returns true if there are any filters applied currently on logcat. */ public boolean isFiltered() { return StringUtil.isNotEmpty(myCurrentFilterName) && !NO_FILTERS.equals(myCurrentFilterName); } public void createAndSelectFilterByPackage(@NotNull String packageName) { AndroidConfiguredLogFilters.MyFilterEntry f = AndroidConfiguredLogFilters.getInstance(myProject).getFilterForPackage(packageName, true); updateConfiguredFilters(f.getName()); } private void updateConfiguredFilters(@NotNull String defaultSelection) { final AndroidConfiguredLogFilters filters = AndroidConfiguredLogFilters.getInstance(myProject); final List<AndroidConfiguredLogFilters.MyFilterEntry> entries = filters.getFilterEntries(); myFilterComboBoxModel.removeAllElements(); myFilterComboBoxModel.addElement(NO_FILTERS); myFilterComboBoxModel.addElement(EDIT_FILTER_CONFIGURATION); for (AndroidConfiguredLogFilters.MyFilterEntry entry : entries) { final String name = entry.getName(); myFilterComboBoxModel.addElement(name); if (name.equals(defaultSelection)) { myFilterComboBoxModel.setSelectedItem(name); } } selectFilter(defaultSelection); } public JPanel getContentPanel() { return myPanel; } @Override public void dispose() { } private class MyRestartAction extends AnAction { public MyRestartAction() { super(AndroidBundle.message("android.restart.logcat.action.text"), AndroidBundle.message("android.restart.logcat.action.description"), AllIcons.Actions.Restart); } @Override public void actionPerformed(AnActionEvent e) { myDevice = null; updateLogConsole(); } } public class AndroidLogConsole extends LogConsoleBase { @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") public AndroidLogConsole(Project project, AndroidLogFilterModel logFilterModel) { super(project, new MyLoggingReader(), "", false, logFilterModel); ConsoleView console = getConsole(); if (console instanceof ConsoleViewImpl) { ConsoleViewImpl c = ((ConsoleViewImpl)console); c.addCustomConsoleAction(new Separator()); c.addCustomConsoleAction(new MyRestartAction()); } } @Override public boolean isActive() { return AndroidLogcatView.this.isActive(); } public void clearLogcat() { IDevice device = getSelectedDevice(); if (device != null) { AndroidLogcatUtil.clearLogcat(myProject, device); } myLogConsole.clear(); } } private class MyFilterSelectedClientAction extends ToggleAction { public MyFilterSelectedClientAction() { super("Only Show Logcat From Selected Process", "", AndroidIcons.Ddms.LogcatAutoFilterSelectedPid); } @Override public boolean isSelected(AnActionEvent e) { return PropertiesComponent.getInstance().getBoolean(FILTER_LOGCAT_WHEN_SELECTION_CHANGES, true); } @Override public void setSelected(AnActionEvent e, boolean state) { // update state so that future changes to selected client don't trigger a filter action PropertiesComponent.getInstance().setValue(FILTER_LOGCAT_WHEN_SELECTION_CHANGES, Boolean.valueOf(state).toString()); if (myDeviceContext != null) { // reset or apply filter depending on current state createAndSelectFilterForClient(state ? myDeviceContext.getSelectedClient() : null); } } } }