1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17 package com.android.ide.eclipse.ddms; 18 19 import com.android.ddmlib.AndroidDebugBridge; 20 import com.android.ddmlib.Client; 21 import com.android.ddmlib.DdmConstants; 22 import com.android.ddmlib.DdmPreferences; 23 import com.android.ddmlib.IDevice; 24 import com.android.ddmlib.Log; 25 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; 26 import com.android.ddmlib.Log.ILogOutput; 27 import com.android.ddmlib.Log.LogLevel; 28 import com.android.ddmuilib.DdmUiPreferences; 29 import com.android.ddmuilib.DevicePanel.IUiSelectionListener; 30 import com.android.ide.eclipse.ddms.preferences.PreferenceInitializer; 31 import com.android.ide.eclipse.ddms.views.DeviceView; 32 33 import org.eclipse.core.runtime.Preferences; 34 import org.eclipse.core.runtime.Preferences.IPropertyChangeListener; 35 import org.eclipse.core.runtime.Preferences.PropertyChangeEvent; 36 import org.eclipse.jface.dialogs.MessageDialog; 37 import org.eclipse.jface.preference.IPreferenceStore; 38 import org.eclipse.swt.SWTException; 39 import org.eclipse.swt.graphics.Color; 40 import org.eclipse.swt.widgets.Display; 41 import org.eclipse.swt.widgets.Shell; 42 import org.eclipse.ui.IWorkbench; 43 import org.eclipse.ui.console.ConsolePlugin; 44 import org.eclipse.ui.console.IConsole; 45 import org.eclipse.ui.console.MessageConsole; 46 import org.eclipse.ui.console.MessageConsoleStream; 47 import org.eclipse.ui.plugin.AbstractUIPlugin; 48 import org.osgi.framework.BundleContext; 49 50 import java.io.File; 51 import java.util.ArrayList; 52 import java.util.Calendar; 53 54 /** 55 * The activator class controls the plug-in life cycle 56 */ 57 public final class DdmsPlugin extends AbstractUIPlugin implements IDeviceChangeListener, 58 IUiSelectionListener { 59 60 61 // The plug-in ID 62 public static final String PLUGIN_ID = "com.android.ide.eclipse.ddms"; // $NON-NLS-1$ 63 64 private static final String ADB_LOCATION = PLUGIN_ID + ".adb"; // $NON-NLS-1$ 65 66 /** The singleton instance */ 67 private static DdmsPlugin sPlugin; 68 69 /** Location of the adb command line executable */ 70 private static String sAdbLocation; 71 private static String sToolsFolder; 72 private static String sHprofConverter; 73 74 /** 75 * Debug Launcher for already running apps 76 */ 77 private static IDebugLauncher sRunningAppDebugLauncher; 78 79 80 /** Console for DDMS log message */ 81 private MessageConsole mDdmsConsole; 82 83 /** Image loader object */ 84 private ImageLoader mLoader; 85 86 private IDevice mCurrentDevice; 87 private Client mCurrentClient; 88 private boolean mListeningToUiSelection = false; 89 90 private final ArrayList<ISelectionListener> mListeners = new ArrayList<ISelectionListener>(); 91 92 private Color mRed; 93 94 private boolean mDdmlibInitialized; 95 96 /** 97 * Interface to provide debugger launcher for running apps. 98 */ 99 public interface IDebugLauncher { debug(String packageName, int port)100 public boolean debug(String packageName, int port); 101 } 102 103 /** 104 * Classes which implement this interface provide methods that deals 105 * with {@link IDevice} and {@link Client} selectionchanges. 106 */ 107 public interface ISelectionListener { 108 109 /** 110 * Sent when a new {@link Client} is selected. 111 * @param selectedClient The selected client. If null, no clients are selected. 112 */ selectionChanged(Client selectedClient)113 public void selectionChanged(Client selectedClient); 114 115 /** 116 * Sent when a new {@link IDevice} is selected. 117 * @param selectedDevice the selected device. If null, no devices are selected. 118 */ selectionChanged(IDevice selectedDevice)119 public void selectionChanged(IDevice selectedDevice); 120 } 121 122 /** 123 * The constructor 124 */ DdmsPlugin()125 public DdmsPlugin() { 126 sPlugin = this; 127 } 128 129 /* 130 * (non-Javadoc) 131 * 132 * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) 133 */ 134 @Override start(BundleContext context)135 public void start(BundleContext context) throws Exception { 136 super.start(context); 137 138 final Display display = getDisplay(); 139 140 // get the eclipse store 141 final IPreferenceStore eclipseStore = getPreferenceStore(); 142 143 AndroidDebugBridge.addDeviceChangeListener(this); 144 145 DdmUiPreferences.setStore(eclipseStore); 146 147 //DdmUiPreferences.displayCharts(); 148 149 // set the consoles. 150 mDdmsConsole = new MessageConsole("DDMS", null); // $NON-NLS-1$ 151 ConsolePlugin.getDefault().getConsoleManager().addConsoles( 152 new IConsole[] { 153 mDdmsConsole 154 }); 155 156 final MessageConsoleStream consoleStream = mDdmsConsole.newMessageStream(); 157 final MessageConsoleStream errorConsoleStream = mDdmsConsole.newMessageStream(); 158 mRed = new Color(display, 0xFF, 0x00, 0x00); 159 160 // because this can be run, in some cases, by a non UI thread, and because 161 // changing the console properties update the UI, we need to make this change 162 // in the UI thread. 163 display.asyncExec(new Runnable() { 164 public void run() { 165 errorConsoleStream.setColor(mRed); 166 } 167 }); 168 169 // set up the ddms log to use the ddms console. 170 Log.setLogOutput(new ILogOutput() { 171 public void printLog(LogLevel logLevel, String tag, String message) { 172 if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) { 173 printToStream(errorConsoleStream, tag, message); 174 ConsolePlugin.getDefault().getConsoleManager().showConsoleView(mDdmsConsole); 175 } else { 176 printToStream(consoleStream, tag, message); 177 } 178 } 179 180 public void printAndPromptLog(final LogLevel logLevel, final String tag, 181 final String message) { 182 printLog(logLevel, tag, message); 183 // dialog box only run in UI thread.. 184 display.asyncExec(new Runnable() { 185 public void run() { 186 Shell shell = display.getActiveShell(); 187 if (logLevel == LogLevel.ERROR) { 188 MessageDialog.openError(shell, tag, message); 189 } else { 190 MessageDialog.openWarning(shell, tag, message); 191 } 192 } 193 }); 194 } 195 196 }); 197 198 // create the loader that's able to load the images 199 mLoader = new ImageLoader(this); 200 201 // set the listener for the preference change 202 Preferences prefs = getPluginPreferences(); 203 prefs.addPropertyChangeListener(new IPropertyChangeListener() { 204 public void propertyChange(PropertyChangeEvent event) { 205 // get the name of the property that changed. 206 String property = event.getProperty(); 207 208 if (PreferenceInitializer.ATTR_DEBUG_PORT_BASE.equals(property)) { 209 DdmPreferences.setDebugPortBase( 210 eclipseStore.getInt(PreferenceInitializer.ATTR_DEBUG_PORT_BASE)); 211 } else if (PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT.equals(property)) { 212 DdmPreferences.setSelectedDebugPort( 213 eclipseStore.getInt(PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT)); 214 } else if (PreferenceInitializer.ATTR_THREAD_INTERVAL.equals(property)) { 215 DdmUiPreferences.setThreadRefreshInterval( 216 eclipseStore.getInt(PreferenceInitializer.ATTR_THREAD_INTERVAL)); 217 } else if (PreferenceInitializer.ATTR_LOG_LEVEL.equals(property)) { 218 DdmPreferences.setLogLevel( 219 eclipseStore.getString(PreferenceInitializer.ATTR_LOG_LEVEL)); 220 } else if (PreferenceInitializer.ATTR_TIME_OUT.equals(property)) { 221 DdmPreferences.setTimeOut( 222 eclipseStore.getInt(PreferenceInitializer.ATTR_TIME_OUT)); 223 } 224 } 225 }); 226 227 // read the adb location from the prefs to attempt to start it properly without 228 // having to wait for ADT to start 229 final boolean adbValid = setAdbLocation(eclipseStore.getString(ADB_LOCATION)); 230 231 // start it in a thread to return from start() asap. 232 new Thread() { 233 @Override 234 public void run() { 235 // init ddmlib if needed 236 getDefault().initDdmlib(); 237 238 // create and start the first bridge 239 if (adbValid) { 240 AndroidDebugBridge.createBridge(sAdbLocation, true /* forceNewBridge */); 241 } 242 } 243 }.start(); 244 } 245 getDisplay()246 public static Display getDisplay() { 247 IWorkbench bench = sPlugin.getWorkbench(); 248 if (bench != null) { 249 return bench.getDisplay(); 250 } 251 return null; 252 } 253 254 /* 255 * (non-Javadoc) 256 * 257 * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) 258 */ 259 @Override stop(BundleContext context)260 public void stop(BundleContext context) throws Exception { 261 AndroidDebugBridge.removeDeviceChangeListener(this); 262 263 AndroidDebugBridge.terminate(); 264 265 mRed.dispose(); 266 267 sPlugin = null; 268 super.stop(context); 269 } 270 271 /** 272 * Returns the shared instance 273 * 274 * @return the shared instance 275 */ getDefault()276 public static DdmsPlugin getDefault() { 277 return sPlugin; 278 } 279 280 /** Return the image loader for the plugin */ getImageLoader()281 public static ImageLoader getImageLoader() { 282 if (sPlugin != null) { 283 return sPlugin.mLoader; 284 } 285 return null; 286 } 287 getAdb()288 public static String getAdb() { 289 return sAdbLocation; 290 } 291 getToolsFolder()292 public static String getToolsFolder() { 293 return sToolsFolder; 294 } 295 getHprofConverter()296 public static String getHprofConverter() { 297 return sHprofConverter; 298 } 299 300 /** 301 * Stores the adb location. This returns true if the location is an existing file. 302 */ setAdbLocation(String adbLocation)303 private static boolean setAdbLocation(String adbLocation) { 304 File adb = new File(adbLocation); 305 if (adb.isFile()) { 306 sAdbLocation = adbLocation; 307 308 File toolsFolder = adb.getParentFile(); 309 sToolsFolder = toolsFolder.getAbsolutePath(); 310 311 File hprofConverter = new File(toolsFolder, DdmConstants.FN_HPROF_CONVERTER); 312 sHprofConverter = hprofConverter.getAbsolutePath(); 313 314 File traceview = new File(toolsFolder, DdmConstants.FN_TRACEVIEW); 315 DdmUiPreferences.setTraceviewLocation(traceview.getAbsolutePath()); 316 317 return true; 318 } 319 320 return false; 321 } 322 323 /** 324 * Set the location of the adb executable and optionally starts adb 325 * @param adb location of adb 326 * @param startAdb flag to start adb 327 */ setAdb(String adb, boolean startAdb)328 public static void setAdb(String adb, boolean startAdb) { 329 if (adb != null) { 330 if (setAdbLocation(adb)) { 331 // store the location for future ddms only start. 332 sPlugin.getPreferenceStore().setValue(ADB_LOCATION, sAdbLocation); 333 334 // starts the server in a thread in case this is blocking. 335 if (startAdb) { 336 new Thread() { 337 @Override 338 public void run() { 339 // init ddmlib if needed 340 getDefault().initDdmlib(); 341 342 // create and start the bridge 343 AndroidDebugBridge.createBridge(sAdbLocation, 344 false /* forceNewBridge */); 345 } 346 }.start(); 347 } 348 } 349 } 350 } 351 initDdmlib()352 private synchronized void initDdmlib() { 353 if (mDdmlibInitialized == false) { 354 // set the preferences. 355 PreferenceInitializer.setupPreferences(); 356 357 // init the lib 358 AndroidDebugBridge.init(true /* debugger support */); 359 360 mDdmlibInitialized = true; 361 } 362 } 363 364 /** 365 * Sets the launcher responsible for connecting the debugger to running applications. 366 * @param launcher The launcher. 367 */ setRunningAppDebugLauncher(IDebugLauncher launcher)368 public static void setRunningAppDebugLauncher(IDebugLauncher launcher) { 369 sRunningAppDebugLauncher = launcher; 370 371 // if the process view is already running, give it the launcher. 372 // This method could be called from a non ui thread, so we make sure to do that 373 // in the ui thread. 374 Display display = getDisplay(); 375 if (display != null && display.isDisposed() == false) { 376 display.asyncExec(new Runnable() { 377 public void run() { 378 DeviceView dv = DeviceView.getInstance(); 379 if (dv != null) { 380 dv.setDebugLauncher(sRunningAppDebugLauncher); 381 } 382 } 383 }); 384 } 385 } 386 getRunningAppDebugLauncher()387 public static IDebugLauncher getRunningAppDebugLauncher() { 388 return sRunningAppDebugLauncher; 389 } 390 addSelectionListener(ISelectionListener listener)391 public synchronized void addSelectionListener(ISelectionListener listener) { 392 mListeners.add(listener); 393 394 // notify the new listener of the current selection 395 listener.selectionChanged(mCurrentDevice); 396 listener.selectionChanged(mCurrentClient); 397 } 398 removeSelectionListener(ISelectionListener listener)399 public synchronized void removeSelectionListener(ISelectionListener listener) { 400 mListeners.remove(listener); 401 } 402 setListeningState(boolean state)403 public synchronized void setListeningState(boolean state) { 404 mListeningToUiSelection = state; 405 } 406 407 /** 408 * Sent when the a device is connected to the {@link AndroidDebugBridge}. 409 * <p/> 410 * This is sent from a non UI thread. 411 * @param device the new device. 412 * 413 * @see IDeviceChangeListener#deviceConnected(IDevice) 414 */ deviceConnected(IDevice device)415 public void deviceConnected(IDevice device) { 416 // if we are listening to selection coming from the ui, then we do nothing, as 417 // any change in the devices/clients, will be handled by the UI, and we'll receive 418 // selection notification through our implementation of IUiSelectionListener. 419 if (mListeningToUiSelection == false) { 420 if (mCurrentDevice == null) { 421 handleDefaultSelection(device); 422 } 423 } 424 } 425 426 /** 427 * Sent when the a device is disconnected to the {@link AndroidDebugBridge}. 428 * <p/> 429 * This is sent from a non UI thread. 430 * @param device the new device. 431 * 432 * @see IDeviceChangeListener#deviceDisconnected(IDevice) 433 */ deviceDisconnected(IDevice device)434 public void deviceDisconnected(IDevice device) { 435 // if we are listening to selection coming from the ui, then we do nothing, as 436 // any change in the devices/clients, will be handled by the UI, and we'll receive 437 // selection notification through our implementation of IUiSelectionListener. 438 if (mListeningToUiSelection == false) { 439 // test if the disconnected device was the default selection. 440 if (mCurrentDevice == device) { 441 // try to find a new device 442 AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); 443 if (bridge != null) { 444 // get the device list 445 IDevice[] devices = bridge.getDevices(); 446 447 // check if we still have devices 448 if (devices.length == 0) { 449 handleDefaultSelection((IDevice)null); 450 } else { 451 handleDefaultSelection(devices[0]); 452 } 453 } else { 454 handleDefaultSelection((IDevice)null); 455 } 456 } 457 } 458 } 459 460 /** 461 * Sent when a device data changed, or when clients are started/terminated on the device. 462 * <p/> 463 * This is sent from a non UI thread. 464 * @param device the device that was updated. 465 * @param changeMask the mask indicating what changed. 466 * 467 * @see IDeviceChangeListener#deviceChanged(IDevice) 468 */ deviceChanged(IDevice device, int changeMask)469 public void deviceChanged(IDevice device, int changeMask) { 470 // if we are listening to selection coming from the ui, then we do nothing, as 471 // any change in the devices/clients, will be handled by the UI, and we'll receive 472 // selection notification through our implementation of IUiSelectionListener. 473 if (mListeningToUiSelection == false) { 474 475 // check if this is our device 476 if (device == mCurrentDevice) { 477 if (mCurrentClient == null) { 478 handleDefaultSelection(device); 479 } else { 480 // get the clients and make sure ours is still in there. 481 Client[] clients = device.getClients(); 482 boolean foundClient = false; 483 for (Client client : clients) { 484 if (client == mCurrentClient) { 485 foundClient = true; 486 break; 487 } 488 } 489 490 // if we haven't found our client, lets look for a new one 491 if (foundClient == false) { 492 mCurrentClient = null; 493 handleDefaultSelection(device); 494 } 495 } 496 } 497 } 498 } 499 500 /** 501 * Sent when a new {@link IDevice} and {@link Client} are selected. 502 * @param selectedDevice the selected device. If null, no devices are selected. 503 * @param selectedClient The selected client. If null, no clients are selected. 504 */ selectionChanged(IDevice selectedDevice, Client selectedClient)505 public synchronized void selectionChanged(IDevice selectedDevice, Client selectedClient) { 506 if (mCurrentDevice != selectedDevice) { 507 mCurrentDevice = selectedDevice; 508 509 // notify of the new default device 510 for (ISelectionListener listener : mListeners) { 511 listener.selectionChanged(mCurrentDevice); 512 } 513 } 514 515 if (mCurrentClient != selectedClient) { 516 mCurrentClient = selectedClient; 517 518 // notify of the new default client 519 for (ISelectionListener listener : mListeners) { 520 listener.selectionChanged(mCurrentClient); 521 } 522 } 523 } 524 525 /** 526 * Handles a default selection of a {@link IDevice} and {@link Client}. 527 * @param device the selected device 528 */ handleDefaultSelection(final IDevice device)529 private void handleDefaultSelection(final IDevice device) { 530 // because the listener expect to receive this from the UI thread, and this is called 531 // from the AndroidDebugBridge notifications, we need to run this in the UI thread. 532 try { 533 Display display = getDisplay(); 534 535 display.asyncExec(new Runnable() { 536 public void run() { 537 // set the new device if different. 538 boolean newDevice = false; 539 if (mCurrentDevice != device) { 540 mCurrentDevice = device; 541 newDevice = true; 542 543 // notify of the new default device 544 for (ISelectionListener listener : mListeners) { 545 listener.selectionChanged(mCurrentDevice); 546 } 547 } 548 549 if (device != null) { 550 // if this is a device switch or the same device but we didn't find a valid 551 // client the last time, we go look for a client to use again. 552 if (newDevice || mCurrentClient == null) { 553 // now get the new client 554 Client[] clients = device.getClients(); 555 if (clients.length > 0) { 556 handleDefaultSelection(clients[0]); 557 } else { 558 handleDefaultSelection((Client)null); 559 } 560 } 561 } else { 562 handleDefaultSelection((Client)null); 563 } 564 } 565 }); 566 } catch (SWTException e) { 567 // display is disposed. Do nothing since we're quitting anyway. 568 } 569 } 570 handleDefaultSelection(Client client)571 private void handleDefaultSelection(Client client) { 572 mCurrentClient = client; 573 574 // notify of the new default client 575 for (ISelectionListener listener : mListeners) { 576 listener.selectionChanged(mCurrentClient); 577 } 578 } 579 580 /** 581 * Prints a message, associated with a project to the specified stream 582 * @param stream The stream to write to 583 * @param tag The tag associated to the message. Can be null 584 * @param message The message to print. 585 */ printToStream(MessageConsoleStream stream, String tag, String message)586 private static synchronized void printToStream(MessageConsoleStream stream, String tag, 587 String message) { 588 String dateTag = getMessageTag(tag); 589 590 stream.print(dateTag); 591 stream.println(message); 592 } 593 594 /** 595 * Creates a string containing the current date/time, and the tag 596 * @param tag The tag associated to the message. Can be null 597 * @return The dateTag 598 */ getMessageTag(String tag)599 private static String getMessageTag(String tag) { 600 Calendar c = Calendar.getInstance(); 601 602 if (tag == null) { 603 return String.format("[%1$tF %1$tT]", c); 604 } 605 606 return String.format("[%1$tF %1$tT - %2$s]", c, tag); 607 } 608 } 609