/* * Copyright 2000-2011 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.actions; import com.android.builder.model.Variant; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ddmlib.IDevice; import com.android.tools.idea.ddms.adb.AdbService; import com.android.tools.idea.gradle.IdeaAndroidProject; import com.intellij.execution.*; import com.intellij.execution.configurations.ConfigurationFactory; import com.intellij.execution.executors.DefaultDebugExecutor; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.remote.RemoteConfiguration; import com.intellij.execution.remote.RemoteConfigurationType; import com.intellij.execution.ui.RunContentDescriptor; import com.intellij.facet.ProjectFacetManager; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.psi.XmlRecursiveElementVisitor; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlElement; import com.intellij.ui.DoubleClickListener; import com.intellij.ui.JBDefaultTreeCellRenderer; import com.intellij.ui.TreeSpeedSearch; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.content.Content; import com.intellij.ui.treeStructure.Tree; import com.intellij.util.NotNullFunction; import com.intellij.util.containers.HashSet; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.TreeUtil; import com.intellij.util.ui.update.MergingUpdateQueue; import com.intellij.util.ui.update.Update; import org.jetbrains.android.compiler.AndroidCompileUtil; import org.jetbrains.android.dom.manifest.Manifest; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.sdk.AndroidSdkUtils; 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 javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import java.awt.*; import java.awt.event.*; import java.util.Collection; import java.util.List; import java.util.Set; /** * @author Eugene.Kudelevsky */ public class AndroidProcessChooserDialog extends DialogWrapper { private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.actions.AndroidProcessChooserDialog"); @NonNls private static final String DEBUGGABLE_PROCESS_PROPERTY = "DEBUGGABLE_PROCESS"; @NonNls private static final String SHOW_ALL_PROCESSES_PROPERTY = "SHOW_ALL_PROCESSES"; @NonNls private static final String DEBUGGABLE_DEVICE_PROPERTY = "DEBUGGABLE_DEVICE"; @NonNls private static final String RUN_CONFIGURATION_NAME_PATTERN = "Android Debugger (%s)"; private final Project myProject; private JPanel myContentPanel; private Tree myProcessTree; private JBCheckBox myShowAllProcessesCheckBox; private final MergingUpdateQueue myUpdatesQueue; private final AndroidDebugBridge.IClientChangeListener myClientChangeListener; private final AndroidDebugBridge.IDeviceChangeListener myDeviceChangeListener; protected AndroidProcessChooserDialog(@NotNull Project project) { super(project); setTitle("Choose Process"); myProject = project; myUpdatesQueue = new MergingUpdateQueue("AndroidProcessChooserDialogUpdatingQueue", 500, true, MergingUpdateQueue.ANY_COMPONENT, myProject); final String showAllProcessesStr = PropertiesComponent.getInstance(project).getValue(SHOW_ALL_PROCESSES_PROPERTY); final boolean showAllProcesses = Boolean.parseBoolean(showAllProcessesStr); myShowAllProcessesCheckBox.setSelected(showAllProcesses); doUpdateTree(showAllProcesses); myClientChangeListener = new AndroidDebugBridge.IClientChangeListener() { @Override public void clientChanged(Client client, int changeMask) { updateTree(); } }; AndroidDebugBridge.addClientChangeListener(myClientChangeListener); myDeviceChangeListener = new AndroidDebugBridge.IDeviceChangeListener() { @Override public void deviceConnected(IDevice device) { updateTree(); } @Override public void deviceDisconnected(IDevice device) { updateTree(); } @Override public void deviceChanged(IDevice device, int changeMask) { updateTree(); } }; AndroidDebugBridge.addDeviceChangeListener(myDeviceChangeListener); myShowAllProcessesCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { updateTree(); } }); myProcessTree.addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { getOKAction().setEnabled(getSelectedDevice() != null && getSelectedClient() != null); } }); new TreeSpeedSearch(myProcessTree) { @Override protected boolean isMatchingElement(Object element, String pattern) { if (element instanceof TreePath) { Object lastComponent = ((TreePath)element).getLastPathComponent(); if (lastComponent instanceof DefaultMutableTreeNode) { Object userObject = ((DefaultMutableTreeNode)lastComponent).getUserObject(); if (userObject instanceof Client) { String pkg = ((Client)userObject).getClientData().getClientDescription(); return pkg != null && pkg.contains(pattern); } } } return false; } }; myProcessTree.setCellRenderer(new JBDefaultTreeCellRenderer(myProcessTree) { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (value instanceof DefaultMutableTreeNode) { final Object userObject = ((DefaultMutableTreeNode)value).getUserObject(); if (userObject instanceof IDevice) { value = ((IDevice)userObject).getName(); } else if (userObject instanceof Client) { final ClientData clientData = ((Client)userObject).getClientData(); String description = clientData.getClientDescription(); if (clientData.isValidUserId() && clientData.getUserId() != 0) { description += " (user " + Integer.toString(clientData.getUserId()) + ")"; } value = description; } } return super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); } @Override public Icon getLeafIcon() { return null; } @Override public Icon getOpenIcon() { return null; } @Override public Icon getClosedIcon() { return null; } }); new DoubleClickListener() { @Override protected boolean onDoubleClick(MouseEvent event) { if (isOKActionEnabled()) { doOKAction(); return true; } return false; } }.installOn(myProcessTree); myProcessTree.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER && isOKActionEnabled()) { doOKAction(); } } }); init(); } @Override public JComponent getPreferredFocusedComponent() { return myProcessTree; } @Override protected void dispose() { super.dispose(); AndroidDebugBridge.removeDeviceChangeListener(myDeviceChangeListener); AndroidDebugBridge.removeClientChangeListener(myClientChangeListener); } private void updateTree() { final boolean showAllProcesses = myShowAllProcessesCheckBox.isSelected(); myUpdatesQueue.queue(new Update(AndroidProcessChooserDialog.this) { @Override public void run() { final AndroidDebugBridge debugBridge = AndroidSdkUtils.getDebugBridge(myProject); if (debugBridge != null && AdbService.isDdmsCorrupted(debugBridge)) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { Messages.showErrorDialog(myContentPanel, AndroidBundle.message("ddms.corrupted.error")); AndroidProcessChooserDialog.this.close(1); } }); return; } doUpdateTree(showAllProcesses); } @Override public boolean canEat(Update update) { return true; } }); } private void doUpdateTree(boolean showAllProcesses) { final AndroidDebugBridge debugBridge = AndroidSdkUtils.getDebugBridge(myProject); final DefaultMutableTreeNode root = new DefaultMutableTreeNode(); final DefaultTreeModel model = new DefaultTreeModel(root); if (debugBridge == null) { myProcessTree.setModel(model); return; } final Set<String> processNames = collectAllProcessNames(myProject); final PropertiesComponent properties = PropertiesComponent.getInstance(myProject); final String prevProcess = properties.getValue(DEBUGGABLE_PROCESS_PROPERTY); final String prevDevice = properties.getValue(DEBUGGABLE_DEVICE_PROPERTY); TreeNode selectedDeviceNode = null; TreeNode selectedClientNode = null; Object[] firstTreePath = null; final IDevice[] devices = debugBridge.getDevices(); for (IDevice device : devices) { final DefaultMutableTreeNode deviceNode = new DefaultMutableTreeNode(device); root.add(deviceNode); for (Client client : device.getClients()) { final String clientDescription = client.getClientData().getClientDescription(); if (clientDescription != null && (showAllProcesses || isRelatedProcess(processNames, clientDescription))) { final DefaultMutableTreeNode clientNode = new DefaultMutableTreeNode(client); deviceNode.add(clientNode); final String deviceName = device.getName(); if (clientDescription.equals(prevProcess) && (selectedDeviceNode == null || deviceName.equals(prevDevice))) { selectedClientNode = clientNode; selectedDeviceNode = deviceNode; } if (firstTreePath == null) { firstTreePath = new Object[]{root, deviceNode, clientNode}; } } } } final Object[] pathToSelect = selectedDeviceNode != null ? new Object[]{root, selectedDeviceNode, selectedClientNode} : firstTreePath; UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { myProcessTree.setModel(model); if (pathToSelect != null) { myProcessTree.getSelectionModel().setSelectionPath(new TreePath(pathToSelect)); } else { getOKAction().setEnabled(false); } TreeUtil.expandAll(myProcessTree); } }); } private boolean isRelatedProcess(Set<String> processNames, String clientDescription) { final String lc = clientDescription.toLowerCase(); for (String processName : processNames) { if (lc.startsWith(processName)) { return true; } } return false; } @NotNull private static Set<String> collectAllProcessNames(Project project) { final List<AndroidFacet> facets = ProjectFacetManager.getInstance(project).getFacets(AndroidFacet.ID); final Set<String> result = new HashSet<String>(); for (AndroidFacet facet : facets) { final String packageName = AndroidCompileUtil.getAaptManifestPackage(facet); if (packageName != null) { result.add(packageName.toLowerCase()); } final Manifest manifest = facet.getManifest(); if (manifest != null) { final XmlElement xmlElement = manifest.getXmlElement(); if (xmlElement != null) { collectProcessNames(xmlElement, result); } } final IdeaAndroidProject androidProject = facet.getIdeaAndroidProject(); if (androidProject != null) { collectApplicationIds(androidProject, result); } } return result; } private static void collectProcessNames(XmlElement xmlElement, final Set<String> result) { xmlElement.accept(new XmlRecursiveElementVisitor() { @Override public void visitXmlAttribute(XmlAttribute attribute) { if ("process".equals(attribute.getLocalName())) { final String value = attribute.getValue(); if (value != null) { result.add(value.toLowerCase()); } } } }); } private static void collectApplicationIds(IdeaAndroidProject androidProject, Set<String> result) { final Collection<Variant> allVariants = androidProject.getDelegate().getVariants(); for (Variant v : allVariants) { String applicationId = v.getMergedFlavor().getApplicationId(); if (applicationId != null) { result.add(applicationId); } } } @Override protected JComponent createCenterPanel() { return myContentPanel; } @Override protected void doOKAction() { final PropertiesComponent properties = PropertiesComponent.getInstance(myProject); final IDevice selectedDevice = getSelectedDevice(); if (selectedDevice == null) { return; } final Client selectedClient = getSelectedClient(); if (selectedClient == null) { return; } super.doOKAction(); properties.setValue(DEBUGGABLE_DEVICE_PROPERTY, selectedDevice.getName()); properties.setValue(DEBUGGABLE_PROCESS_PROPERTY, selectedClient.getClientData().getClientDescription()); properties.setValue(SHOW_ALL_PROCESSES_PROPERTY, Boolean.toString(myShowAllProcessesCheckBox.isSelected())); final String debugPort = Integer.toString(selectedClient.getDebuggerListenPort()); closeOldSessionAndRun(debugPort); } @Override protected String getDimensionServiceKey() { return "AndroidProcessChooserDialog"; } private void closeOldSessionAndRun(final String debugPort) { final String configurationName = getRunConfigurationName(debugPort); final Collection<RunContentDescriptor> descriptors = ExecutionHelper.findRunningConsoleByTitle(myProject, new NotNullFunction<String, Boolean>() { @NotNull @Override public Boolean fun(String title) { return configurationName.equals(title); } }); if (descriptors.size() > 0) { final RunContentDescriptor descriptor = descriptors.iterator().next(); final ProcessHandler processHandler = descriptor.getProcessHandler(); final Content content = descriptor.getAttachedContent(); if (processHandler != null && content != null) { final Executor executor = DefaultDebugExecutor.getDebugExecutorInstance(); if (processHandler.isProcessTerminated()) { ExecutionManager.getInstance(myProject).getContentManager() .removeRunContent(executor, descriptor); } else { content.getManager().setSelectedContent(content); ToolWindow window = ToolWindowManager.getInstance(myProject).getToolWindow(executor.getToolWindowId()); window.activate(null, false, true); return; } } } runSession(debugPort); } private void runSession(String debugPort) { final RunnerAndConfigurationSettings settings = createRunConfiguration(myProject, debugPort); ProgramRunnerUtil.executeConfiguration(myProject, settings, DefaultDebugExecutor.getDebugExecutorInstance()); } private static RunnerAndConfigurationSettings createRunConfiguration(Project project, String debugPort) { final RemoteConfigurationType remoteConfigurationType = RemoteConfigurationType.getInstance(); final ConfigurationFactory factory = remoteConfigurationType.getFactory(); final RunnerAndConfigurationSettings runSettings = RunManager.getInstance(project).createRunConfiguration(getRunConfigurationName(debugPort), factory); final RemoteConfiguration configuration = (RemoteConfiguration)runSettings.getConfiguration(); configuration.HOST = "localhost"; configuration.PORT = debugPort; configuration.USE_SOCKET_TRANSPORT = true; configuration.SERVER_MODE = false; return runSettings; } @NotNull private static String getRunConfigurationName(String debugPort) { return String.format(RUN_CONFIGURATION_NAME_PATTERN, debugPort); } @Nullable private IDevice getSelectedDevice() { final TreePath selectionPath = myProcessTree.getSelectionPath(); if (selectionPath == null || selectionPath.getPathCount() < 2) { return null; } DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)selectionPath.getPathComponent(1); final Object obj = selectedNode.getUserObject(); return obj instanceof IDevice ? (IDevice)obj : null; } @Nullable private Client getSelectedClient() { final TreePath selectionPath = myProcessTree.getSelectionPath(); if (selectionPath == null || selectionPath.getPathCount() < 3) { return null; } DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)selectionPath.getPathComponent(2); final Object obj = selectedNode.getUserObject(); return obj instanceof Client ? (Client)obj : null; } }