From d4963dc4347c871be5faa76920709001490e1d2a Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Wed, 24 Feb 2010 09:41:10 -0800 Subject: [PATCH] snapshot --- .gitignore | 5 + androidprefs/.classpath | 6 + androidprefs/.gitignore | 2 + androidprefs/.project | 17 + androidprefs/Android.mk | 17 + androidprefs/src/Android.mk | 24 + .../src/com/android/prefs/AndroidLocation.java | 108 ++ anttasks/.classpath | 9 + anttasks/.gitignore | 2 + anttasks/.project | 17 + anttasks/Android.mk | 17 + anttasks/src/Android.mk | 29 + anttasks/src/com/android/ant/AaptExecLoopTask.java | 273 +++ anttasks/src/com/android/ant/ApkBuilderTask.java | 357 ++++ anttasks/src/com/android/ant/SetupTask.java | 336 ++++ anttasks/src/com/android/ant/XPathTask.java | 82 + apkbuilder/.classpath | 8 + apkbuilder/.gitignore | 2 + apkbuilder/.project | 17 + apkbuilder/Android.mk | 18 + apkbuilder/etc/Android.mk | 22 + apkbuilder/etc/apkbuilder | 81 + apkbuilder/etc/apkbuilder.bat | 48 + apkbuilder/etc/manifest.txt | 1 + apkbuilder/src/Android.mk | 29 + .../src/com/android/apkbuilder/ApkBuilder.java | 119 ++ .../apkbuilder/internal/ApkBuilderImpl.java | 460 +++++ archquery/.classpath | 6 + archquery/.gitignore | 1 + archquery/.project | 17 + archquery/Android.mk | 17 + archquery/etc/manifest.txt | 1 + archquery/src/Android.mk | 25 + archquery/src/com/android/archquery/Main.java | 72 + changes.txt | 14 + ddms/.gitignore | 4 + ddms/Android.mk | 5 + ddms/MODULE_LICENSE_APACHE2 | 0 ddms/app/.classpath | 12 + ddms/app/.project | 17 + ddms/app/Android.mk | 5 + ddms/app/README | 11 + ddms/app/etc/Android.mk | 8 + ddms/app/etc/ddms | 108 ++ ddms/app/etc/ddms.bat | 69 + ddms/app/etc/manifest.txt | 1 + ddms/app/src/Android.mk | 22 + ddms/app/src/com/android/ddms/AboutDialog.java | 157 ++ .../src/com/android/ddms/DebugPortProvider.java | 163 ++ .../src/com/android/ddms/DeviceCommandDialog.java | 423 +++++ .../android/ddms/DropdownSelectionListener.java | 80 + ddms/app/src/com/android/ddms/Main.java | 139 ++ ddms/app/src/com/android/ddms/PrefsDialog.java | 529 ++++++ .../com/android/ddms/StaticPortConfigDialog.java | 394 ++++ .../src/com/android/ddms/StaticPortEditDialog.java | 330 ++++ ddms/app/src/com/android/ddms/UIThread.java | 1703 +++++++++++++++++ ddms/app/src/resources/images/ddms-icon.png | Bin 0 -> 23410 bytes ddms/app/src/resources/images/ddms-logo.png | Bin 0 -> 12285 bytes ddms/libs/Android.mk | 5 + ddms/libs/ddmlib/.classpath | 6 + ddms/libs/ddmlib/.project | 17 + ddms/libs/ddmlib/Android.mk | 4 + ddms/libs/ddmlib/src/Android.mk | 11 + .../ddmlib/src/com/android/ddmlib/AdbHelper.java | 731 ++++++++ .../src/com/android/ddmlib/AllocationInfo.java | 71 + .../src/com/android/ddmlib/AndroidDebugBridge.java | 1052 +++++++++++ .../src/com/android/ddmlib/BadPacketException.java | 35 + .../src/com/android/ddmlib/ChunkHandler.java | 222 +++ .../libs/ddmlib/src/com/android/ddmlib/Client.java | 837 +++++++++ .../ddmlib/src/com/android/ddmlib/ClientData.java | 694 +++++++ .../src/com/android/ddmlib/DdmConstants.java | 59 + .../src/com/android/ddmlib/DdmPreferences.java | 166 ++ .../src/com/android/ddmlib/DebugPortManager.java | 72 + .../ddmlib/src/com/android/ddmlib/Debugger.java | 353 ++++ .../libs/ddmlib/src/com/android/ddmlib/Device.java | 510 +++++ .../src/com/android/ddmlib/DeviceMonitor.java | 867 +++++++++ .../src/com/android/ddmlib/EmulatorConsole.java | 751 ++++++++ .../src/com/android/ddmlib/FileListingService.java | 767 ++++++++ .../src/com/android/ddmlib/GetPropReceiver.java | 74 + .../src/com/android/ddmlib/HandleAppName.java | 94 + .../ddmlib/src/com/android/ddmlib/HandleExit.java | 76 + .../ddmlib/src/com/android/ddmlib/HandleHeap.java | 597 ++++++ .../ddmlib/src/com/android/ddmlib/HandleHello.java | 177 ++ .../src/com/android/ddmlib/HandleNativeHeap.java | 305 +++ .../src/com/android/ddmlib/HandleProfiling.java | 304 +++ .../ddmlib/src/com/android/ddmlib/HandleTest.java | 86 + .../src/com/android/ddmlib/HandleThread.java | 379 ++++ .../ddmlib/src/com/android/ddmlib/HandleWait.java | 91 + .../ddmlib/src/com/android/ddmlib/HeapSegment.java | 446 +++++ .../ddmlib/src/com/android/ddmlib/IDevice.java | 264 +++ .../com/android/ddmlib/IShellOutputReceiver.java | 44 + .../src/com/android/ddmlib/IStackTraceInfo.java | 29 + .../ddmlib/src/com/android/ddmlib/JdwpPacket.java | 371 ++++ ddms/libs/ddmlib/src/com/android/ddmlib/Log.java | 351 ++++ .../src/com/android/ddmlib/MonitorThread.java | 780 ++++++++ .../src/com/android/ddmlib/MultiLineReceiver.java | 130 ++ .../com/android/ddmlib/NativeAllocationInfo.java | 277 +++ .../com/android/ddmlib/NativeLibraryMapInfo.java | 73 + .../com/android/ddmlib/NativeStackCallInfo.java | 95 + .../src/com/android/ddmlib/NullOutputReceiver.java | 50 + .../ddmlib/src/com/android/ddmlib/RawImage.java | 227 +++ .../ddmlib/src/com/android/ddmlib/SyncService.java | 980 ++++++++++ .../ddmlib/src/com/android/ddmlib/ThreadInfo.java | 139 ++ .../src/com/android/ddmlib/log/EventContainer.java | 461 +++++ .../src/com/android/ddmlib/log/EventLogParser.java | 577 ++++++ .../android/ddmlib/log/EventValueDescription.java | 214 +++ .../com/android/ddmlib/log/GcEventContainer.java | 347 ++++ .../android/ddmlib/log/InvalidTypeException.java | 74 + .../ddmlib/log/InvalidValueTypeException.java | 78 + .../src/com/android/ddmlib/log/LogReceiver.java | 247 +++ .../ddmlib/testrunner/ITestRunListener.java | 85 + .../testrunner/InstrumentationResultParser.java | 409 ++++ .../ddmlib/testrunner/RemoteAndroidTestRunner.java | 251 +++ .../android/ddmlib/testrunner/TestIdentifier.java | 76 + .../src/com/android/ddmlib/utils/ArrayHelper.java | 90 + .../InstrumentationResultParserTest.java | 288 +++ .../testrunner/RemoteAndroidTestRunnerTest.java | 282 +++ ddms/libs/ddmuilib/.classpath | 10 + ddms/libs/ddmuilib/.project | 17 + ddms/libs/ddmuilib/Android.mk | 4 + ddms/libs/ddmuilib/README | 11 + ddms/libs/ddmuilib/src/Android.mk | 22 + .../src/com/android/ddmuilib/Addr2Line.java | 281 +++ .../src/com/android/ddmuilib/AllocationPanel.java | 486 +++++ .../src/com/android/ddmuilib/BackgroundThread.java | 50 + .../src/com/android/ddmuilib/BaseHeapPanel.java | 193 ++ .../com/android/ddmuilib/ClientDisplayPanel.java | 33 + .../src/com/android/ddmuilib/DdmUiPreferences.java | 79 + .../src/com/android/ddmuilib/DevicePanel.java | 760 ++++++++ .../com/android/ddmuilib/EmulatorControlPanel.java | 1454 +++++++++++++++ .../src/com/android/ddmuilib/HeapPanel.java | 1294 +++++++++++++ .../src/com/android/ddmuilib/IImageLoader.java | 45 + .../com/android/ddmuilib/ITableFocusListener.java | 38 + .../src/com/android/ddmuilib/ImageHelper.java | 86 + .../src/com/android/ddmuilib/ImageLoader.java | 62 + .../src/com/android/ddmuilib/InfoPanel.java | 197 ++ .../src/com/android/ddmuilib/NativeHeapPanel.java | 1633 ++++++++++++++++ .../ddmuilib/src/com/android/ddmuilib/Panel.java | 49 + .../src/com/android/ddmuilib/PortFieldEditor.java | 73 + .../src/com/android/ddmuilib/ScreenShotDialog.java | 308 +++ .../android/ddmuilib/SelectionDependentPanel.java | 78 + .../src/com/android/ddmuilib/StackTracePanel.java | 258 +++ .../com/android/ddmuilib/SyncProgressMonitor.java | 55 + .../src/com/android/ddmuilib/SysinfoPanel.java | 582 ++++++ .../src/com/android/ddmuilib/TableHelper.java | 203 ++ .../src/com/android/ddmuilib/TablePanel.java | 128 ++ .../src/com/android/ddmuilib/ThreadPanel.java | 572 ++++++ .../android/ddmuilib/actions/ICommonAction.java | 42 + .../android/ddmuilib/actions/ToolItemAction.java | 68 + .../com/android/ddmuilib/annotation/UiThread.java | 31 + .../android/ddmuilib/annotation/WorkerThread.java | 31 + .../com/android/ddmuilib/console/DdmConsole.java | 91 + .../com/android/ddmuilib/console/IDdmConsole.java | 47 + .../ddmuilib/explorer/DeviceContentProvider.java | 167 ++ .../android/ddmuilib/explorer/DeviceExplorer.java | 791 ++++++++ .../ddmuilib/explorer/FileLabelProvider.java | 152 ++ .../android/ddmuilib/handler/BaseFileHandler.java | 199 ++ .../ddmuilib/handler/MethodProfilingHandler.java | 173 ++ .../ddmuilib/location/CoordinateControls.java | 243 +++ .../com/android/ddmuilib/location/GpxParser.java | 373 ++++ .../com/android/ddmuilib/location/KmlParser.java | 210 +++ .../android/ddmuilib/location/LocationPoint.java | 53 + .../ddmuilib/location/TrackContentProvider.java | 45 + .../ddmuilib/location/TrackLabelProvider.java | 81 + .../com/android/ddmuilib/location/TrackPoint.java | 34 + .../com/android/ddmuilib/location/WayPoint.java | 42 + .../ddmuilib/location/WayPointContentProvider.java | 43 + .../ddmuilib/location/WayPointLabelProvider.java | 73 + .../ddmuilib/log/event/BugReportImporter.java | 89 + .../ddmuilib/log/event/DisplayFilteredLog.java | 55 + .../android/ddmuilib/log/event/DisplayGraph.java | 422 +++++ .../com/android/ddmuilib/log/event/DisplayLog.java | 379 ++++ .../android/ddmuilib/log/event/DisplaySync.java | 299 +++ .../ddmuilib/log/event/DisplaySyncHistogram.java | 177 ++ .../ddmuilib/log/event/DisplaySyncPerf.java | 219 +++ .../android/ddmuilib/log/event/EventDisplay.java | 971 ++++++++++ .../ddmuilib/log/event/EventDisplayOptions.java | 955 ++++++++++ .../ddmuilib/log/event/EventLogImporter.java | 82 + .../android/ddmuilib/log/event/EventLogPanel.java | 926 +++++++++ .../ddmuilib/log/event/EventValueSelector.java | 628 +++++++ .../ddmuilib/log/event/OccurrenceRenderer.java | 90 + .../com/android/ddmuilib/log/event/SyncCommon.java | 172 ++ .../android/ddmuilib/logcat/EditFilterDialog.java | 353 ++++ .../src/com/android/ddmuilib/logcat/LogColors.java | 27 + .../src/com/android/ddmuilib/logcat/LogFilter.java | 555 ++++++ .../src/com/android/ddmuilib/logcat/LogPanel.java | 1584 ++++++++++++++++ ddms/libs/ddmuilib/src/resources/images/add.png | Bin 0 -> 146 bytes .../libs/ddmuilib/src/resources/images/android.png | Bin 0 -> 3609 bytes .../ddmuilib/src/resources/images/backward.png | Bin 0 -> 136 bytes ddms/libs/ddmuilib/src/resources/images/clear.png | Bin 0 -> 217 bytes ddms/libs/ddmuilib/src/resources/images/d.png | Bin 0 -> 638 bytes .../ddmuilib/src/resources/images/debug-attach.png | Bin 0 -> 156 bytes .../ddmuilib/src/resources/images/debug-error.png | Bin 0 -> 222 bytes .../ddmuilib/src/resources/images/debug-wait.png | Bin 0 -> 156 bytes ddms/libs/ddmuilib/src/resources/images/delete.png | Bin 0 -> 107 bytes ddms/libs/ddmuilib/src/resources/images/device.png | Bin 0 -> 135 bytes ddms/libs/ddmuilib/src/resources/images/down.png | Bin 0 -> 141 bytes ddms/libs/ddmuilib/src/resources/images/e.png | Bin 0 -> 511 bytes ddms/libs/ddmuilib/src/resources/images/edit.png | Bin 0 -> 223 bytes ddms/libs/ddmuilib/src/resources/images/empty.png | Bin 0 -> 75 bytes .../ddmuilib/src/resources/images/emulator.png | Bin 0 -> 287 bytes ddms/libs/ddmuilib/src/resources/images/file.png | Bin 0 -> 157 bytes ddms/libs/ddmuilib/src/resources/images/folder.png | Bin 0 -> 123 bytes .../libs/ddmuilib/src/resources/images/forward.png | Bin 0 -> 137 bytes ddms/libs/ddmuilib/src/resources/images/gc.png | Bin 0 -> 165 bytes ddms/libs/ddmuilib/src/resources/images/halt.png | Bin 0 -> 197 bytes ddms/libs/ddmuilib/src/resources/images/heap.png | Bin 0 -> 222 bytes ddms/libs/ddmuilib/src/resources/images/hprof.png | Bin 0 -> 317 bytes ddms/libs/ddmuilib/src/resources/images/i.png | Bin 0 -> 498 bytes .../ddmuilib/src/resources/images/importBug.png | Bin 0 -> 191 bytes ddms/libs/ddmuilib/src/resources/images/load.png | Bin 0 -> 163 bytes ddms/libs/ddmuilib/src/resources/images/pause.png | Bin 0 -> 98 bytes ddms/libs/ddmuilib/src/resources/images/play.png | Bin 0 -> 138 bytes ddms/libs/ddmuilib/src/resources/images/pull.png | Bin 0 -> 329 bytes ddms/libs/ddmuilib/src/resources/images/push.png | Bin 0 -> 228 bytes ddms/libs/ddmuilib/src/resources/images/save.png | Bin 0 -> 240 bytes ddms/libs/ddmuilib/src/resources/images/thread.png | Bin 0 -> 121 bytes .../src/resources/images/tracing_start.png | Bin 0 -> 227 bytes .../ddmuilib/src/resources/images/tracing_stop.png | Bin 0 -> 217 bytes ddms/libs/ddmuilib/src/resources/images/up.png | Bin 0 -> 134 bytes ddms/libs/ddmuilib/src/resources/images/v.png | Bin 0 -> 587 bytes ddms/libs/ddmuilib/src/resources/images/w.png | Bin 0 -> 681 bytes .../libs/ddmuilib/src/resources/images/warning.png | Bin 0 -> 147 bytes draw9patch/Android.mk | 17 + draw9patch/MODULE_LICENSE_APACHE2 | 0 draw9patch/etc/Android.mk | 20 + draw9patch/etc/draw9patch | 63 + draw9patch/etc/draw9patch.bat | 46 + draw9patch/etc/manifest.txt | 2 + draw9patch/src/Android.mk | 26 + .../src/com/android/draw9patch/Application.java | 55 + .../draw9patch/graphics/GraphicsUtilities.java | 96 + .../com/android/draw9patch/ui/GradientPanel.java | 47 + .../android/draw9patch/ui/ImageEditorPanel.java | 1180 ++++++++++++ .../draw9patch/ui/ImageTransferHandler.java | 86 + .../src/com/android/draw9patch/ui/MainFrame.java | 174 ++ .../com/android/draw9patch/ui/OpenFilePanel.java | 51 + .../com/android/draw9patch/ui/PngFileFilter.java | 32 + .../draw9patch/ui/action/BackgroundAction.java | 29 + .../android/draw9patch/ui/action/ExitAction.java | 44 + .../android/draw9patch/ui/action/OpenAction.java | 43 + .../android/draw9patch/ui/action/SaveAction.java | 43 + draw9patch/src/resources/images/checker.png | Bin 0 -> 1889 bytes draw9patch/src/resources/images/drop.png | Bin 0 -> 5479 bytes dumpeventlog/.classpath | 7 + dumpeventlog/.project | 17 + dumpeventlog/Android.mk | 5 + dumpeventlog/etc/Android.mk | 8 + dumpeventlog/etc/dumpeventlog | 81 + dumpeventlog/etc/manifest.txt | 1 + dumpeventlog/src/Android.mk | 14 + .../src/com/android/dumpeventlog/DumpEventLog.java | 145 ++ eclipse/README_WINDOWS.txt | 32 + eclipse/buildConfig/allElements.xml | 60 + eclipse/buildConfig/build.properties | 238 +++ eclipse/buildConfig/buildUpdateSite.xml | 13 + eclipse/buildConfig/customTargets.xml | 195 ++ eclipse/changes.txt | 253 +++ .../features/com.android.ide.eclipse.adt/.project | 17 + .../com.android.ide.eclipse.adt/build.properties | 1 + .../com.android.ide.eclipse.adt/feature.xml | 154 ++ .../features/com.android.ide.eclipse.ddms/.project | 17 + .../com.android.ide.eclipse.ddms/build.properties | 1 + .../com.android.ide.eclipse.ddms/feature.xml | 245 +++ .../com.android.ide.eclipse.tests/.project | 17 + .../com.android.ide.eclipse.tests/build.properties | 1 + .../com.android.ide.eclipse.tests/feature.xml | 30 + eclipse/plugins/.gitignore | 59 + .../plugins/com.android.ide.eclipse.adt/.classpath | 18 + .../plugins/com.android.ide.eclipse.adt/.gitignore | 1 + .../plugins/com.android.ide.eclipse.adt/.project | 30 + .../META-INF/MANIFEST.MF | 131 ++ .../com.android.ide.eclipse.adt/MODULE_LICENSE_EPL | 0 eclipse/plugins/com.android.ide.eclipse.adt/NOTICE | 224 +++ .../plugins/com.android.ide.eclipse.adt/about.ini | 1 + .../com.android.ide.eclipse.adt/build.properties | 20 + .../gscripts/android.view.View.groovy | 39 + .../gscripts/android.widget.AbsoluteLayout.groovy | 63 + .../gscripts/android.widget.LinearLayout.groovy | 82 + .../gscripts/android.widget.ListView.groovy | 46 + .../com.android.ide.eclipse.adt/icons/add.png | Bin 0 -> 146 bytes .../com.android.ide.eclipse.adt/icons/android.png | Bin 0 -> 197 bytes .../icons/android_32x32.png | Bin 0 -> 1390 bytes .../icons/android_app.png | Bin 0 -> 454 bytes .../icons/android_file.png | Bin 0 -> 519 bytes .../icons/android_large.png | Bin 0 -> 1447 bytes .../icons/android_project.png | Bin 0 -> 146 bytes .../icons/androidjunit.png | Bin 0 -> 393 bytes .../icons/avd_manager.png | Bin 0 -> 219 bytes .../com.android.ide.eclipse.adt/icons/az_sort.png | Bin 0 -> 363 bytes .../com.android.ide.eclipse.adt/icons/clipping.png | Bin 0 -> 167 bytes .../com.android.ide.eclipse.adt/icons/delete.png | Bin 0 -> 107 bytes .../icons/dimension.png | Bin 0 -> 320 bytes .../com.android.ide.eclipse.adt/icons/down.png | Bin 0 -> 157 bytes .../com.android.ide.eclipse.adt/icons/dpi.png | Bin 0 -> 302 bytes .../com.android.ide.eclipse.adt/icons/error.png | Bin 0 -> 194 bytes .../com.android.ide.eclipse.adt/icons/keyboard.png | Bin 0 -> 307 bytes .../com.android.ide.eclipse.adt/icons/language.png | Bin 0 -> 287 bytes .../icons/mainLaunchTab.png | Bin 0 -> 308 bytes .../com.android.ide.eclipse.adt/icons/match.png | Bin 0 -> 138 bytes .../com.android.ide.eclipse.adt/icons/mcc.png | Bin 0 -> 463 bytes .../com.android.ide.eclipse.adt/icons/mnc.png | Bin 0 -> 265 bytes .../com.android.ide.eclipse.adt/icons/navpad.png | Bin 0 -> 308 bytes .../icons/new_adt_project.png | Bin 0 -> 664 bytes .../com.android.ide.eclipse.adt/icons/new_xml.png | Bin 0 -> 539 bytes .../icons/orientation.png | Bin 0 -> 325 bytes .../com.android.ide.eclipse.adt/icons/region.png | Bin 0 -> 445 bytes .../icons/text_input.png | Bin 0 -> 321 bytes .../com.android.ide.eclipse.adt/icons/touch.png | Bin 0 -> 344 bytes .../com.android.ide.eclipse.adt/icons/up.png | Bin 0 -> 137 bytes .../com.android.ide.eclipse.adt/icons/warning.png | Bin 0 -> 147 bytes .../plugins/com.android.ide.eclipse.adt/plugin.xml | 701 +++++++ .../src/com/android/ide/eclipse/adt/AdtPlugin.java | 1415 ++++++++++++++ .../android/ide/eclipse/adt/AndroidConstants.java | 234 +++ .../src/com/android/ide/eclipse/adt/Messages.java | 50 + .../adt/editors/layout/gscripts/BaseViewRule.java | 70 + .../adt/editors/layout/gscripts/DropZone.java | 62 + .../adt/editors/layout/gscripts/INodeProxy.java | 92 + .../adt/editors/layout/gscripts/IViewRule.java | 125 ++ .../eclipse/adt/editors/layout/gscripts/Point.java | 30 + .../eclipse/adt/editors/layout/gscripts/Rect.java | 79 + .../ide/eclipse/adt/internal/VersionCheck.java | 117 ++ .../internal/actions/ConvertToAndroidAction.java | 154 ++ .../adt/internal/actions/FixProjectAction.java | 139 ++ .../ide/eclipse/adt/internal/build/ApkBuilder.java | 1318 +++++++++++++ .../adt/internal/build/ApkDeltaVisitor.java | 280 +++ .../eclipse/adt/internal/build/BaseBuilder.java | 956 ++++++++++ .../ide/eclipse/adt/internal/build/Messages.java | 137 ++ .../adt/internal/build/PreCompilerBuilder.java | 1084 +++++++++++ .../internal/build/PreCompilerDeltaVisitor.java | 540 ++++++ .../adt/internal/build/ResourceManagerBuilder.java | 222 +++ .../adt/internal/build/build_messages.properties | 61 + .../adt/internal/editors/AndroidContentAssist.java | 807 ++++++++ .../adt/internal/editors/AndroidEditor.java | 861 +++++++++ .../editors/AndroidSourceViewerConfig.java | 116 ++ .../adt/internal/editors/FirstElementParser.java | 164 ++ .../eclipse/adt/internal/editors/IconFactory.java | 255 +++ .../editors/descriptors/AttributeDescriptor.java | 104 ++ .../AttributeDescriptorLabelProvider.java | 81 + .../descriptors/BooleanAttributeDescriptor.java | 33 + .../editors/descriptors/DescriptorsUtils.java | 850 +++++++++ .../editors/descriptors/DocumentDescriptor.java | 57 + .../editors/descriptors/ElementDescriptor.java | 348 ++++ .../descriptors/EnumAttributeDescriptor.java | 41 + .../descriptors/FlagAttributeDescriptor.java | 85 + .../editors/descriptors/IDescriptorProvider.java | 24 + .../descriptors/ListAttributeDescriptor.java | 71 + .../descriptors/ReferenceAttributeDescriptor.java | 92 + .../descriptors/SeparatorAttributeDescriptor.java | 45 + .../descriptors/TextAttributeDescriptor.java | 137 ++ .../editors/descriptors/TextValueDescriptor.java | 48 + .../descriptors/XmlnsAttributeDescriptor.java | 81 + .../internal/editors/layout/BasePullParser.java | 219 +++ .../editors/layout/ExplodedRenderingHelper.java | 421 +++++ .../editors/layout/IGraphicalLayoutEditor.java | 102 + .../internal/editors/layout/LayoutConstants.java | 65 + .../editors/layout/LayoutContentAssist.java | 33 + .../adt/internal/editors/layout/LayoutEditor.java | 452 +++++ .../editors/layout/LayoutReloadMonitor.java | 282 +++ .../editors/layout/LayoutSourceViewerConfig.java | 30 + .../internal/editors/layout/MatchingStrategy.java | 64 + .../internal/editors/layout/ProjectCallback.java | 165 ++ .../editors/layout/UiElementPullParser.java | 535 ++++++ .../internal/editors/layout/WidgetPullParser.java | 147 ++ .../layout/configuration/ConfigEditDialog.java | 304 +++ .../layout/configuration/ConfigManagerDialog.java | 555 ++++++ .../configuration/ConfigurationComposite.java | 1557 ++++++++++++++++ .../layout/configuration/LayoutCreatorDialog.java | 141 ++ .../descriptors/CustomViewDescriptorService.java | 285 +++ .../layout/descriptors/LayoutDescriptors.java | 372 ++++ .../layout/descriptors/ViewElementDescriptor.java | 181 ++ .../editors/layout/gle1/GraphicalLayoutEditor.java | 1447 +++++++++++++++ .../editors/layout/gle1/PaletteFactory.java | 95 + .../editors/layout/gle1/UiContentOutlinePage.java | 618 ++++++ .../editors/layout/gle1/UiPropertySheetPage.java | 141 ++ .../layout/gle2/CanvasAlternateSelection.java | 73 + .../editors/layout/gle2/CanvasDropListener.java | 294 +++ .../editors/layout/gle2/CanvasSelection.java | 119 ++ .../editors/layout/gle2/CanvasViewInfo.java | 164 ++ .../editors/layout/gle2/ElementDescTransfer.java | 115 ++ .../editors/layout/gle2/GraphicalEditorPart.java | 1195 ++++++++++++ .../internal/editors/layout/gle2/LayoutCanvas.java | 775 ++++++++ .../editors/layout/gle2/PaletteComposite.java | 358 ++++ .../adt/internal/editors/layout/gre/NodeProxy.java | 259 +++ .../internal/editors/layout/gre/RulesEngine.java | 395 ++++ .../editors/layout/parts/DropFeedback.java | 765 ++++++++ .../editors/layout/parts/ElementCreateCommand.java | 100 + .../editors/layout/parts/ElementFigure.java | 81 + .../editors/layout/parts/LayoutFigure.java | 154 ++ .../editors/layout/parts/UiDocumentEditPart.java | 214 +++ .../layout/parts/UiDocumentTreeEditPart.java | 39 + .../editors/layout/parts/UiElementEditPart.java | 347 ++++ .../layout/parts/UiElementTreeEditPart.java | 76 + .../layout/parts/UiElementTreeEditPartFactory.java | 49 + .../layout/parts/UiElementsEditPartFactory.java | 68 + .../editors/layout/parts/UiLayoutEditPart.java | 120 ++ .../editors/layout/parts/UiLayoutTreeEditPart.java | 41 + .../editors/layout/parts/UiViewEditPart.java | 57 + .../editors/layout/parts/UiViewTreeEditPart.java | 32 + .../editors/layout/uimodel/UiViewElementNode.java | 130 ++ .../editors/manifest/ManifestContentAssist.java | 33 + .../internal/editors/manifest/ManifestEditor.java | 388 ++++ .../manifest/ManifestEditorContributor.java | 100 + .../manifest/ManifestSourceViewerConfig.java | 30 + .../descriptors/AndroidManifestDescriptors.java | 578 ++++++ .../ApplicationAttributeDescriptor.java | 45 + .../descriptors/ClassAttributeDescriptor.java | 101 + .../descriptors/ManifestElementDescriptor.java | 96 + .../descriptors/ManifestPkgAttrDescriptor.java | 41 + .../descriptors/PackageAttributeDescriptor.java | 41 + .../descriptors/PostActivityCreationAction.java | 88 + .../descriptors/PostReceiverCreationAction.java | 88 + .../descriptors/ThemeAttributeDescriptor.java | 42 + .../manifest/model/UiClassAttributeNode.java | 689 +++++++ .../manifest/model/UiManifestElementNode.java | 98 + .../manifest/model/UiManifestPkgAttrNode.java | 338 ++++ .../manifest/model/UiPackageAttributeNode.java | 319 ++++ .../manifest/pages/ApplicationAttributesPart.java | 174 ++ .../editors/manifest/pages/ApplicationPage.java | 126 ++ .../editors/manifest/pages/ApplicationToggle.java | 313 ++++ .../manifest/pages/InstrumentationPage.java | 92 + .../editors/manifest/pages/OverviewExportPart.java | 87 + .../editors/manifest/pages/OverviewInfoPart.java | 87 + .../editors/manifest/pages/OverviewLinksPart.java | 125 ++ .../editors/manifest/pages/OverviewPage.java | 157 ++ .../editors/manifest/pages/PermissionPage.java | 101 + .../internal/editors/menu/MenuContentAssist.java | 33 + .../adt/internal/editors/menu/MenuEditor.java | 184 ++ .../editors/menu/MenuSourceViewerConfig.java | 30 + .../adt/internal/editors/menu/MenuTreePage.java | 62 + .../editors/menu/descriptors/MenuDescriptors.java | 196 ++ .../editors/resources/ResourcesContentAssist.java | 33 + .../editors/resources/ResourcesEditor.java | 164 ++ .../resources/ResourcesSourceViewerConfig.java | 30 + .../editors/resources/ResourcesTreePage.java | 85 + .../descriptors/ColorValueDescriptor.java | 41 + .../descriptors/ItemElementDescriptor.java | 55 + .../descriptors/ResourcesDescriptors.java | 283 +++ .../resources/uimodel/UiColorValueNode.java | 80 + .../resources/uimodel/UiItemElementNode.java | 58 + .../editors/ui/EditableDialogCellEditor.java | 458 +++++ .../internal/editors/ui/ErrorImageComposite.java | 47 + .../internal/editors/ui/FlagValueCellEditor.java | 58 + .../internal/editors/ui/ListValueCellEditor.java | 76 + .../editors/ui/ResourceValueCellEditor.java | 59 + .../adt/internal/editors/ui/SectionHelper.java | 348 ++++ .../internal/editors/ui/TextValueCellEditor.java | 43 + .../adt/internal/editors/ui/UiElementPart.java | 283 +++ .../internal/editors/ui/tree/CopyCutAction.java | 220 +++ .../adt/internal/editors/ui/tree/ICommitXml.java | 28 + .../editors/ui/tree/NewItemSelectionDialog.java | 402 ++++ .../adt/internal/editors/ui/tree/PasteAction.java | 124 ++ .../adt/internal/editors/ui/tree/UiActions.java | 385 ++++ .../internal/editors/ui/tree/UiElementDetail.java | 486 +++++ .../ui/tree/UiModelTreeContentProvider.java | 114 ++ .../editors/ui/tree/UiModelTreeLabelProvider.java | 100 + .../adt/internal/editors/ui/tree/UiTreeBlock.java | 898 +++++++++ .../editors/uimodel/IUiSettableAttributeNode.java | 32 + .../editors/uimodel/IUiUpdateListener.java | 47 + .../uimodel/UiAbstractTextAttributeNode.java | 119 ++ .../internal/editors/uimodel/UiAttributeNode.java | 159 ++ .../internal/editors/uimodel/UiDocumentNode.java | 135 ++ .../internal/editors/uimodel/UiElementNode.java | 1531 +++++++++++++++ .../editors/uimodel/UiFlagAttributeNode.java | 310 ++++ .../editors/uimodel/UiListAttributeNode.java | 219 +++ .../editors/uimodel/UiResourceAttributeNode.java | 275 +++ .../editors/uimodel/UiSeparatorAttributeNode.java | 146 ++ .../editors/uimodel/UiTextAttributeNode.java | 194 ++ .../internal/editors/uimodel/UiTextValueNode.java | 118 ++ .../adt/internal/editors/xml/XmlContentAssist.java | 33 + .../adt/internal/editors/xml/XmlEditor.java | 205 ++ .../editors/xml/XmlSourceViewerConfig.java | 30 + .../adt/internal/editors/xml/XmlTreePage.java | 62 + .../editors/xml/descriptors/XmlDescriptors.java | 364 ++++ .../eclipse/adt/internal/launch/AMReceiver.java | 160 ++ .../adt/internal/launch/ActivityLaunchAction.java | 99 + .../eclipse/adt/internal/launch/AndroidLaunch.java | 57 + .../launch/AndroidLaunchConfiguration.java | 159 ++ .../internal/launch/AndroidLaunchController.java | 1612 ++++++++++++++++ .../adt/internal/launch/DelayedLaunchInfo.java | 242 +++ .../adt/internal/launch/DeviceChooserDialog.java | 732 ++++++++ .../adt/internal/launch/EmptyLaunchAction.java | 38 + .../adt/internal/launch/EmulatorConfigTab.java | 482 +++++ .../adt/internal/launch/IAndroidLaunchAction.java | 42 + .../adt/internal/launch/ILaunchController.java | 40 + .../internal/launch/JUnitLaunchConfigDelegate.java | 155 ++ .../adt/internal/launch/LaunchConfigDelegate.java | 421 +++++ .../adt/internal/launch/LaunchConfigTabGroup.java | 40 + .../adt/internal/launch/LaunchShortcut.java | 87 + .../adt/internal/launch/MainLaunchConfigTab.java | 488 +++++ .../launch/junit/AndroidJUnitLaunchAction.java | 263 +++ .../junit/AndroidJUnitLaunchConfigDelegate.java | 232 +++ .../junit/AndroidJUnitLaunchConfigurationTab.java | 997 ++++++++++ .../launch/junit/AndroidJUnitLaunchShortcut.java | 56 + .../launch/junit/AndroidJUnitPropertyTester.java | 130 ++ .../launch/junit/AndroidJUnitTabGroup.java | 42 + .../junit/InstrumentationRunnerValidator.java | 150 ++ .../junit/runtime/AndroidJUnitLaunchInfo.java | 135 ++ .../launch/junit/runtime/AndroidTestReference.java | 63 + .../launch/junit/runtime/RemoteAdtTestRunner.java | 228 +++ .../launch/junit/runtime/TestCaseReference.java | 75 + .../launch/junit/runtime/TestCollector.java | 124 ++ .../launch/junit/runtime/TestSuiteReference.java | 78 + .../eclipse/adt/internal/preferences/AdtPrefs.java | 178 ++ .../preferences/AndroidPreferencePage.java | 226 +++ .../internal/preferences/BuildPreferencePage.java | 207 +++ .../internal/preferences/LaunchPreferencePage.java | 51 + .../eclipse/adt/internal/preferences/Messages.java | 43 + .../internal/preferences/UsagePreferencePage.java | 123 ++ .../adt/internal/preferences/messages.properties | 14 + .../project/AndroidClasspathContainer.java | 60 + .../AndroidClasspathContainerInitializer.java | 647 +++++++ .../internal/project/AndroidManifestParser.java | 977 ++++++++++ .../adt/internal/project/AndroidNature.java | 291 +++ .../adt/internal/project/ApkInstallManager.java | 271 +++ .../adt/internal/project/BaseProjectHelper.java | 440 +++++ .../eclipse/adt/internal/project/ExportHelper.java | 197 ++ .../adt/internal/project/FixLaunchConfig.java | 156 ++ .../adt/internal/project/FolderDecorator.java | 107 ++ .../adt/internal/project/ProjectChooserHelper.java | 129 ++ .../adt/internal/project/ProjectHelper.java | 768 ++++++++ .../adt/internal/project/XmlErrorHandler.java | 125 ++ .../internal/properties/AndroidPropertyPage.java | 133 ++ .../extractstring/ExtractStringAction.java | 180 ++ .../extractstring/ExtractStringContribution.java | 53 + .../extractstring/ExtractStringDescriptor.java | 71 + .../extractstring/ExtractStringInputPage.java | 548 ++++++ .../extractstring/ExtractStringRefactoring.java | 1466 +++++++++++++++ .../extractstring/ExtractStringWizard.java | 50 + .../extractstring/ReplaceStringsVisitor.java | 457 +++++ .../extractstring/XmlStringFileHelper.java | 144 ++ .../adt/internal/resources/AttrsXmlParser.java | 505 +++++ .../internal/resources/DeclareStyleableInfo.java | 186 ++ .../adt/internal/resources/IIdResourceItem.java | 28 + .../internal/resources/IResourceRepository.java | 47 + .../adt/internal/resources/ResourceItem.java | 48 + .../adt/internal/resources/ResourceType.java | 111 ++ .../adt/internal/resources/ViewClassInfo.java | 159 ++ .../configurations/CountryCodeQualifier.java | 152 ++ .../configurations/FolderConfiguration.java | 549 ++++++ .../configurations/KeyboardStateQualifier.java | 218 +++ .../configurations/LanguageQualifier.java | 154 ++ .../configurations/NavigationMethodQualifier.java | 183 ++ .../configurations/NetworkCodeQualifier.java | 163 ++ .../configurations/PixelDensityQualifier.java | 272 +++ .../resources/configurations/RegionQualifier.java | 154 ++ .../configurations/ResourceQualifier.java | 118 ++ .../configurations/ScreenDimensionQualifier.java | 158 ++ .../configurations/ScreenOrientationQualifier.java | 179 ++ .../configurations/ScreenRatioQualifier.java | 185 ++ .../configurations/ScreenSizeQualifier.java | 189 ++ .../configurations/TextInputMethodQualifier.java | 183 ++ .../configurations/TouchScreenQualifier.java | 181 ++ .../resources/configurations/VersionQualifier.java | 156 ++ .../manager/CompiledResourcesMonitor.java | 239 +++ .../manager/ConfigurableResourceItem.java | 82 + .../resources/manager/FolderTypeRelationship.java | 164 ++ .../resources/manager/GlobalProjectMonitor.java | 387 ++++ .../internal/resources/manager/IdResourceItem.java | 54 + .../resources/manager/IntArrayWrapper.java | 50 + .../resources/manager/MultiResourceFile.java | 174 ++ .../resources/manager/ProjectClassLoader.java | 251 +++ .../resources/manager/ProjectResourceItem.java | 91 + .../resources/manager/ProjectResources.java | 880 +++++++++ .../adt/internal/resources/manager/Resource.java | 46 + .../internal/resources/manager/ResourceFile.java | 100 + .../internal/resources/manager/ResourceFolder.java | 255 +++ .../resources/manager/ResourceFolderType.java | 73 + .../resources/manager/ResourceManager.java | 659 +++++++ .../resources/manager/SingleResourceFile.java | 155 ++ .../resources/manager/files/FileWrapper.java | 86 + .../resources/manager/files/FolderWrapper.java | 73 + .../resources/manager/files/IAbstractFile.java | 44 + .../resources/manager/files/IAbstractFolder.java | 38 + .../resources/manager/files/IAbstractResource.java | 34 + .../resources/manager/files/IFileWrapper.java | 68 + .../resources/manager/files/IFolderWrapper.java | 74 + .../eclipse/adt/internal/sdk/AndroidJarLoader.java | 431 +++++ .../adt/internal/sdk/AndroidTargetData.java | 328 ++++ .../adt/internal/sdk/AndroidTargetParser.java | 708 +++++++ .../ide/eclipse/adt/internal/sdk/DexWrapper.java | 169 ++ .../internal/sdk/FrameworkResourceRepository.java | 76 + .../adt/internal/sdk/IAndroidClassLoader.java | 81 + .../ide/eclipse/adt/internal/sdk/LayoutDevice.java | 263 +++ .../adt/internal/sdk/LayoutDeviceHandler.java | 179 ++ .../adt/internal/sdk/LayoutDeviceManager.java | 396 ++++ .../eclipse/adt/internal/sdk/LayoutDevicesXsd.java | 134 ++ .../adt/internal/sdk/LayoutParamsParser.java | 378 ++++ .../ide/eclipse/adt/internal/sdk/LoadStatus.java | 24 + .../android/ide/eclipse/adt/internal/sdk/Sdk.java | 759 ++++++++ .../adt/internal/sdk/WidgetClassLoader.java | 334 ++++ .../eclipse/adt/internal/sdk/layout-devices.xsd | 327 ++++ .../adt/internal/ui/ConfigurationSelector.java | 1518 +++++++++++++++ .../eclipse/adt/internal/ui/EclipseUiHelper.java | 64 + .../adt/internal/ui/IUpdateWizardDialog.java | 30 + .../adt/internal/ui/ReferenceChooserDialog.java | 363 ++++ .../eclipse/adt/internal/ui/ResourceChooser.java | 293 +++ .../adt/internal/ui/ResourceContentProvider.java | 110 ++ .../adt/internal/ui/ResourceExplorerView.java | 338 ++++ .../adt/internal/ui/ResourceLabelProvider.java | 138 ++ .../eclipse/adt/internal/ui/WizardDialogEx.java | 46 + .../internal/wizards/actions/AvdManagerAction.java | 68 + .../adt/internal/wizards/actions/ExportAction.java | 65 + .../wizards/actions/ExportWizardAction.java | 57 + .../internal/wizards/actions/NewProjectAction.java | 36 + .../wizards/actions/NewTestProjectAction.java | 34 + .../internal/wizards/actions/NewXmlFileAction.java | 36 + .../internal/wizards/actions/OpenWizardAction.java | 178 ++ .../adt/internal/wizards/export/ExportWizard.java | 697 +++++++ .../adt/internal/wizards/export/KeyCheckPage.java | 446 +++++ .../internal/wizards/export/KeyCreationPage.java | 332 ++++ .../internal/wizards/export/KeySelectionPage.java | 266 +++ .../wizards/export/KeystoreSelectionPage.java | 260 +++ .../internal/wizards/export/ProjectCheckPage.java | 302 +++ .../wizards/newproject/NewProjectCreationPage.java | 1653 +++++++++++++++++ .../wizards/newproject/NewProjectWizard.java | 1092 +++++++++++ .../newproject/NewTestProjectCreationPage.java | 1338 +++++++++++++ .../wizards/newproject/NewTestProjectWizard.java | 30 + .../wizards/newxmlfile/NewXmlFileCreationPage.java | 1243 +++++++++++++ .../wizards/newxmlfile/NewXmlFileWizard.java | 226 +++ .../android/ide/eclipse/adt/messages.properties | 17 + .../templates/AndroidManifest.template | 12 + .../templates/activity.template | 7 + .../templates/icon_hdpi.png | Bin 0 -> 4147 bytes .../templates/icon_ldpi.png | Bin 0 -> 1723 bytes .../templates/icon_mdpi.png | Bin 0 -> 2574 bytes .../templates/java_file.template | 13 + .../templates/launcher_intent_filter.template | 1 + .../templates/layout.template | 13 + .../templates/preference_intent_filter.template | 1 + .../templates/string.template | 1 + .../templates/strings.template | 5 + .../templates/test_instrumentation.template | 1 + .../templates/test_uses-library.template | 1 + .../templates/uses-sdk.template | 1 + .../com.android.ide.eclipse.ddms/.classpath | 10 + .../plugins/com.android.ide.eclipse.ddms/.project | 28 + .../META-INF/MANIFEST.MF | 36 + .../MODULE_LICENSE_APACHE2 | 0 .../com.android.ide.eclipse.ddms/build.properties | 10 + .../com.android.ide.eclipse.ddms/icons/.gitignore | 31 + .../com.android.ide.eclipse.ddms/icons/android.png | Bin 0 -> 197 bytes .../com.android.ide.eclipse.ddms/icons/capture.png | Bin 0 -> 696 bytes .../com.android.ide.eclipse.ddms/libs/.gitignore | 1 + .../com.android.ide.eclipse.ddms/plugin.xml | 109 ++ .../src/com/android/.gitignore | 2 + .../com/android/ide/eclipse/ddms/CommonAction.java | 69 + .../com/android/ide/eclipse/ddms/DdmsPlugin.java | 608 ++++++ .../com/android/ide/eclipse/ddms/ImageLoader.java | 67 + .../com/android/ide/eclipse/ddms/Perspective.java | 83 + .../ddms/preferences/LogCatPreferencePage.java | 74 + .../ddms/preferences/PreferenceInitializer.java | 114 ++ .../eclipse/ddms/preferences/PreferencePage.java | 94 + .../ide/eclipse/ddms/views/AllocTrackerView.java | 47 + .../android/ide/eclipse/ddms/views/DeviceView.java | 577 ++++++ .../eclipse/ddms/views/EmulatorControlView.java | 43 + .../ide/eclipse/ddms/views/EventLogView.java | 114 ++ .../ide/eclipse/ddms/views/FileExplorerView.java | 166 ++ .../android/ide/eclipse/ddms/views/HeapView.java | 47 + .../android/ide/eclipse/ddms/views/LogCatView.java | 462 +++++ .../ide/eclipse/ddms/views/NativeHeapView.java | 47 + .../ddms/views/SelectionDependentViewPart.java | 69 + .../android/ide/eclipse/ddms/views/TableView.java | 96 + .../android/ide/eclipse/ddms/views/ThreadView.java | 47 + .../com.android.ide.eclipse.tests/.classpath | 15 + .../com.android.ide.eclipse.tests/.gitignore | 1 + .../plugins/com.android.ide.eclipse.tests/.project | 29 + .../META-INF/MANIFEST.MF | 23 + .../MODULE_LICENSE_EPL | 0 .../plugins/com.android.ide.eclipse.tests/NOTICE | 224 +++ .../com.android.ide.eclipse.tests/README.txt | 101 + .../com.android.ide.eclipse.tests/build.properties | 17 + .../not_source_folder/jar/example/Class1.java | 35 + .../not_source_folder/jar/example/Class2.java | 21 + .../com.android.ide.eclipse.tests/prefs.template | 3 + .../launch/JUnitLaunchConfigDelegateTest.java | 112 ++ .../adt/internal/sdk/TestLayoutDevicesXsd.java | 329 ++++ .../ide/eclipse/adt/internal/sdk/config_sample.xml | 137 ++ .../newproject/StubProjectCreationPage.java | 92 + .../adt/wizards/newproject/StubProjectWizard.java | 117 ++ .../com/android/ide/eclipse/tests/AdtTestData.java | 96 + .../com/android/ide/eclipse/tests/AllTests.java | 41 + .../ide/eclipse/tests/AndroidTestPlugin.java | 69 + .../ide/eclipse/tests/EclipseTestCollector.java | 105 ++ .../com/android/ide/eclipse/tests/FuncTests.java | 48 + .../ide/eclipse/tests/GroovyTestsSuite.java | 46 + .../com/android/ide/eclipse/tests/SdkTestCase.java | 101 + .../com/android/ide/eclipse/tests/UnitTests.java | 59 + .../layoutRendering/ApiDemosRenderingTest.java | 253 +++ .../sampleProjects/AndroidManifestWriter.java | 131 ++ .../sampleProjects/SampleProjectTest.java | 245 +++ .../ide/eclipse/tests/groovytests/TestGroovy.java | 175 ++ .../eclipse/tests/groovytests/compile_error.groovy | 34 + .../tests/groovytests/invalid_interface.groovy | 24 + .../eclipse/tests/groovytests/simple_test.groovy | 85 + .../plugins/com.android.ide.eclipse.tests/test.xml | 76 + .../com.android.ide.eclipse.tests/unittest.xml | 54 + .../adt/internal/build/BaseBuilderTest.java | 37 + .../editors/descriptors/DescriptorsUtilsTest.java | 127 ++ .../editors/layout/ExplodeRenderingHelperTest.java | 256 +++ .../editors/layout/UiElementPullParserTest.java | 248 +++ .../editors/manifest/model/UiElementNodeTest.java | 211 +++ .../internal/editors/mock/MockNamedNodeMap.java | 104 ++ .../adt/internal/editors/mock/MockNodeList.java | 60 + .../adt/internal/editors/mock/MockXmlNode.java | 282 +++ .../configurations/CountryCodeQualifierTest.java | 58 + .../configurations/KeyboardStateQualifierTest.java | 64 + .../configurations/LanguageQualifierTest.java | 56 + .../NavigationMethodQualifierTest.java | 72 + .../configurations/NetworkCodeQualifierTest.java | 58 + .../configurations/PixelDensityQualifierTest.java | 59 + .../configurations/RegionQualifierTest.java | 57 + .../ScreenDimensionQualifierTest.java | 60 + .../ScreenOrientationQualifierTest.java | 74 + .../TextInputMethodQualifierTest.java | 74 + .../configurations/TouchScreenQualifierTest.java | 73 + .../editors/resources/manager/ConfigMatchTest.java | 272 +++ .../resources/manager/QualifierListTest.java | 76 + .../project/AndroidManifestParserTest.java | 104 ++ .../adt/internal/project/ProjectHelperTest.java | 68 + .../adt/internal/resources/AttrsXmlParserTest.java | 135 ++ .../adt/internal/sdk/AndroidJarLoaderTest.java | 166 ++ .../adt/internal/sdk/LayoutParamsParserTest.java | 180 ++ .../ide/eclipse/mock/ClasspathEntryMock.java | 89 + .../com/android/ide/eclipse/mock/FileMock.java | 473 +++++ .../com/android/ide/eclipse/mock/FolderMock.java | 460 +++++ .../android/ide/eclipse/mock/JavaProjectMock.java | 414 +++++ .../com/android/ide/eclipse/mock/ProjectMock.java | 517 ++++++ .../testdata/AndroidManifest-instrumentation.xml | 18 + .../eclipse/testdata/AndroidManifest-testapp.xml | 17 + .../android/ide/eclipse/testdata/jar_example.jar | Bin 0 -> 1829 bytes .../ide/eclipse/testdata/jar_example.jardesc | 16 + .../android/ide/eclipse/testdata/mock_attrs.xml | 340 ++++ .../unittests/mock_android/view/View.java | 21 + .../unittests/mock_android/view/ViewGroup.java | 29 + .../mock_android/widget/LinearLayout.java | 27 + .../unittests/mock_android/widget/TableLayout.java | 27 + eclipse/scripts/_mk_icons.sh | 55 + eclipse/scripts/build_plugins.sh | 225 +++ eclipse/scripts/build_server.sh | 118 ++ eclipse/scripts/build_update_site.sh | 34 + eclipse/scripts/collect_sources_for_sdk.py | 170 ++ eclipse/scripts/create_adt_symlinks.sh | 62 + eclipse/scripts/create_all_symlinks.sh | 36 + eclipse/scripts/create_bridge_symlinks.sh | 38 + eclipse/scripts/create_ddms_symlinks.sh | 79 + eclipse/scripts/create_test_symlinks.sh | 84 + eclipse/scripts/gen_icon.py | 71 + eclipse/scripts/setup_eclipse.sh | 67 + eclipse/scripts/update_version.sh | 37 + eclipse/sites/external/.project | 17 + eclipse/sites/external/index.html | 60 + eclipse/sites/external/site.xml | 17 + eclipse/sites/external/web/site.css | 12 + eclipse/sites/external/web/site.xsl | 214 +++ eclipse/sites/internal/.project | 17 + eclipse/sites/internal/index.html | 60 + eclipse/sites/internal/site.xml | 25 + eclipse/sites/internal/web/site.css | 12 + eclipse/sites/internal/web/site.xsl | 214 +++ emulator/keymaps/AVRCP.kl | 7 + emulator/keymaps/Android.mk | 18 + emulator/keymaps/qwerty.kcm | 64 + emulator/keymaps/qwerty.kl | 89 + emulator/keymaps/qwerty2.kcm | 81 + emulator/mksdcard/Android.mk | 11 + emulator/mksdcard/mksdcard.c | 304 +++ emulator/mksdcard/vfat-empty-32MB.img.gz | Bin 0 -> 32807 bytes emulator/qemud/Android.mk | 16 + emulator/qemud/qemud.c | 1715 +++++++++++++++++ emulator/qtools/Android.mk | 157 ++ emulator/qtools/armdis.cpp | 905 +++++++++ emulator/qtools/armdis.h | 45 + emulator/qtools/bb2sym.cpp | 140 ++ emulator/qtools/bb_dump.cpp | 47 + emulator/qtools/bbprof.cpp | 222 +++ emulator/qtools/bitvector.h | 40 + emulator/qtools/callstack.h | 775 ++++++++ emulator/qtools/check_stack.cpp | 270 +++ emulator/qtools/check_trace.cpp | 61 + emulator/qtools/coverage.cpp | 153 ++ emulator/qtools/decoder.cpp | 278 +++ emulator/qtools/decoder.h | 28 + emulator/qtools/dmtrace.cpp | 255 +++ emulator/qtools/dmtrace.h | 61 + emulator/qtools/dump_regions.cpp | 59 + emulator/qtools/exc_dump.cpp | 28 + emulator/qtools/gtrace.cpp | 152 ++ emulator/qtools/gtrace.h | 69 + emulator/qtools/hash_table.h | 219 +++ emulator/qtools/hist_trace.cpp | 64 + emulator/qtools/opcode.cpp | 204 ++ emulator/qtools/opcode.h | 166 ++ emulator/qtools/parse_options-inl.h | 155 ++ emulator/qtools/parse_options.cpp | 119 ++ emulator/qtools/parse_options.h | 32 + emulator/qtools/post_trace.cpp | 151 ++ emulator/qtools/profile_pid.cpp | 94 + emulator/qtools/profile_trace.cpp | 131 ++ emulator/qtools/q2dm.cpp | 274 +++ emulator/qtools/q2g.cpp | 108 ++ emulator/qtools/read_addr.cpp | 29 + emulator/qtools/read_elf.cpp | 210 +++ emulator/qtools/read_elf.h | 20 + emulator/qtools/read_method.cpp | 137 ++ emulator/qtools/read_pid.cpp | 71 + emulator/qtools/read_trace.cpp | 165 ++ emulator/qtools/stack_dump.cpp | 156 ++ emulator/qtools/tests/common_head.mk | 25 + emulator/qtools/tests/common_tail.mk | 3 + emulator/qtools/tests/gtrace/Makefile | 18 + emulator/qtools/tests/gtrace/test.c | 201 ++ emulator/qtools/tests/macros.h | 93 + emulator/qtools/tests/tests.ld | 10 + emulator/qtools/thumbdis.cpp | 503 +++++ emulator/qtools/trace_reader.cpp | 1201 ++++++++++++ emulator/qtools/trace_reader.h | 1559 ++++++++++++++++ emulator/qtools/trace_reader_base.h | 332 ++++ emulator/sensors/Android.mk | 29 + emulator/sensors/sensors_qemu.c | 597 ++++++ emulator/skins/HVGA/arrow_down.png | Bin 0 -> 449 bytes emulator/skins/HVGA/arrow_left.png | Bin 0 -> 825 bytes emulator/skins/HVGA/arrow_right.png | Bin 0 -> 795 bytes emulator/skins/HVGA/arrow_up.png | Bin 0 -> 453 bytes emulator/skins/HVGA/background_land.png | Bin 0 -> 2949 bytes emulator/skins/HVGA/background_port.png | Bin 0 -> 3053 bytes emulator/skins/HVGA/button.png | Bin 0 -> 592 bytes emulator/skins/HVGA/controls.png | Bin 0 -> 19390 bytes emulator/skins/HVGA/hardware.ini | 2 + emulator/skins/HVGA/key.png | Bin 0 -> 154 bytes emulator/skins/HVGA/keyboard.png | Bin 0 -> 20001 bytes emulator/skins/HVGA/layout | 443 +++++ emulator/skins/HVGA/select.png | Bin 0 -> 384 bytes emulator/skins/HVGA/spacebar.png | Bin 0 -> 192 bytes emulator/skins/QVGA/arrow_down.png | Bin 0 -> 449 bytes emulator/skins/QVGA/arrow_left.png | Bin 0 -> 825 bytes emulator/skins/QVGA/arrow_right.png | Bin 0 -> 795 bytes emulator/skins/QVGA/arrow_up.png | Bin 0 -> 453 bytes emulator/skins/QVGA/background_land.png | Bin 0 -> 2717 bytes emulator/skins/QVGA/background_port.png | Bin 0 -> 2586 bytes emulator/skins/QVGA/button.png | Bin 0 -> 592 bytes emulator/skins/QVGA/controls.png | Bin 0 -> 19390 bytes emulator/skins/QVGA/hardware.ini | 2 + emulator/skins/QVGA/key.png | Bin 0 -> 154 bytes emulator/skins/QVGA/keyboard.png | Bin 0 -> 20001 bytes emulator/skins/QVGA/layout | 439 +++++ emulator/skins/QVGA/select.png | Bin 0 -> 384 bytes emulator/skins/QVGA/spacebar.png | Bin 0 -> 192 bytes emulator/skins/WQVGA400/arrow_down.png | Bin 0 -> 449 bytes emulator/skins/WQVGA400/arrow_left.png | Bin 0 -> 825 bytes emulator/skins/WQVGA400/arrow_right.png | Bin 0 -> 795 bytes emulator/skins/WQVGA400/arrow_up.png | Bin 0 -> 453 bytes emulator/skins/WQVGA400/background_land.png | Bin 0 -> 108520 bytes emulator/skins/WQVGA400/background_port.png | Bin 0 -> 116810 bytes emulator/skins/WQVGA400/button.png | Bin 0 -> 592 bytes emulator/skins/WQVGA400/controls.png | Bin 0 -> 19390 bytes emulator/skins/WQVGA400/hardware.ini | 2 + emulator/skins/WQVGA400/key.png | Bin 0 -> 154 bytes emulator/skins/WQVGA400/keyboard.png | Bin 0 -> 20001 bytes emulator/skins/WQVGA400/layout | 438 +++++ emulator/skins/WQVGA400/select.png | Bin 0 -> 384 bytes emulator/skins/WQVGA400/spacebar.png | Bin 0 -> 192 bytes emulator/skins/WQVGA432/arrow_down.png | Bin 0 -> 449 bytes emulator/skins/WQVGA432/arrow_left.png | Bin 0 -> 825 bytes emulator/skins/WQVGA432/arrow_right.png | Bin 0 -> 795 bytes emulator/skins/WQVGA432/arrow_up.png | Bin 0 -> 453 bytes emulator/skins/WQVGA432/background_land.png | Bin 0 -> 2713 bytes emulator/skins/WQVGA432/background_port.png | Bin 0 -> 2766 bytes emulator/skins/WQVGA432/button.png | Bin 0 -> 592 bytes emulator/skins/WQVGA432/controls.png | Bin 0 -> 19390 bytes emulator/skins/WQVGA432/hardware.ini | 2 + emulator/skins/WQVGA432/key.png | Bin 0 -> 154 bytes emulator/skins/WQVGA432/keyboard.png | Bin 0 -> 20001 bytes emulator/skins/WQVGA432/layout | 438 +++++ emulator/skins/WQVGA432/select.png | Bin 0 -> 384 bytes emulator/skins/WQVGA432/spacebar.png | Bin 0 -> 192 bytes emulator/skins/WVGA800/arrow_down.png | Bin 0 -> 449 bytes emulator/skins/WVGA800/arrow_left.png | Bin 0 -> 825 bytes emulator/skins/WVGA800/arrow_right.png | Bin 0 -> 795 bytes emulator/skins/WVGA800/arrow_up.png | Bin 0 -> 453 bytes emulator/skins/WVGA800/background_land.png | Bin 0 -> 4033 bytes emulator/skins/WVGA800/background_port.png | Bin 0 -> 4619 bytes emulator/skins/WVGA800/button.png | Bin 0 -> 592 bytes emulator/skins/WVGA800/controls.png | Bin 0 -> 19390 bytes emulator/skins/WVGA800/hardware.ini | 3 + emulator/skins/WVGA800/key.png | Bin 0 -> 154 bytes emulator/skins/WVGA800/keyboard.png | Bin 0 -> 20001 bytes emulator/skins/WVGA800/layout | 439 +++++ emulator/skins/WVGA800/select.png | Bin 0 -> 384 bytes emulator/skins/WVGA800/spacebar.png | Bin 0 -> 192 bytes emulator/skins/WVGA854/arrow_down.png | Bin 0 -> 449 bytes emulator/skins/WVGA854/arrow_left.png | Bin 0 -> 825 bytes emulator/skins/WVGA854/arrow_right.png | Bin 0 -> 795 bytes emulator/skins/WVGA854/arrow_up.png | Bin 0 -> 453 bytes emulator/skins/WVGA854/background_land.png | Bin 0 -> 4150 bytes emulator/skins/WVGA854/background_port.png | Bin 0 -> 4842 bytes emulator/skins/WVGA854/button.png | Bin 0 -> 592 bytes emulator/skins/WVGA854/controls.png | Bin 0 -> 19390 bytes emulator/skins/WVGA854/hardware.ini | 3 + emulator/skins/WVGA854/key.png | Bin 0 -> 154 bytes emulator/skins/WVGA854/keyboard.png | Bin 0 -> 20001 bytes emulator/skins/WVGA854/layout | 439 +++++ emulator/skins/WVGA854/select.png | Bin 0 -> 384 bytes emulator/skins/WVGA854/spacebar.png | Bin 0 -> 192 bytes emulator/tools/Android.mk | 36 + emulator/tools/qemu-props.c | 114 ++ eventanalyzer/.classpath | 7 + eventanalyzer/.project | 17 + eventanalyzer/Android.mk | 18 + eventanalyzer/etc/Android.mk | 21 + eventanalyzer/etc/eventanalyzer | 73 + eventanalyzer/etc/manifest.txt | 1 + eventanalyzer/src/Android.mk | 27 + .../com/android/eventanalyzer/EventAnalyzer.java | 484 +++++ files/README_add-ons.txt | 2 + files/alias_rules.xml | 56 + files/android.el | 131 ++ files/android_rules.xml | 452 +++++ files/android_test_rules.xml | 104 ++ files/devices.xml | 87 + files/find_java.bat | 80 + files/plugin.prop | 3 + files/post_tools_install.bat | 38 + files/sdk_files_NOTICE.txt | 195 ++ files/tools_source.properties | 2 + hierarchyviewer/Android.mk | 17 + hierarchyviewer/MODULE_LICENSE_APACHE2 | 0 hierarchyviewer/etc/Android.mk | 20 + hierarchyviewer/etc/hierarchyviewer | 63 + hierarchyviewer/etc/hierarchyviewer.bat | 46 + hierarchyviewer/etc/manifest.txt | 2 + hierarchyviewer/src/Android.mk | 30 + .../android/hierarchyviewer/HierarchyViewer.java | 67 + .../hierarchyviewer/device/Configuration.java | 27 + .../hierarchyviewer/device/DeviceBridge.java | 190 ++ .../com/android/hierarchyviewer/device/Window.java | 45 + .../hierarchyviewer/laf/UnifiedContentBorder.java | 43 + .../hierarchyviewer/scene/CaptureLoader.java | 72 + .../hierarchyviewer/scene/ProfilesLoader.java | 77 + .../hierarchyviewer/scene/ViewHierarchyLoader.java | 186 ++ .../hierarchyviewer/scene/ViewHierarchyScene.java | 262 +++ .../android/hierarchyviewer/scene/ViewManager.java | 67 + .../android/hierarchyviewer/scene/ViewNode.java | 203 ++ .../hierarchyviewer/scene/WindowsLoader.java | 87 + .../hierarchyviewer/ui/CaptureRenderer.java | 86 + .../android/hierarchyviewer/ui/LayoutRenderer.java | 177 ++ .../android/hierarchyviewer/ui/ScreenViewer.java | 749 ++++++++ .../com/android/hierarchyviewer/ui/Workspace.java | 1504 +++++++++++++++ .../ui/action/BackgroundAction.java | 29 + .../ui/action/CaptureNodeAction.java | 42 + .../hierarchyviewer/ui/action/ExitAction.java | 48 + .../ui/action/InvalidateAction.java | 42 + .../hierarchyviewer/ui/action/LoadGraphAction.java | 43 + .../ui/action/RefreshWindowsAction.java | 40 + .../ui/action/RequestLayoutAction.java | 42 + .../hierarchyviewer/ui/action/SaveSceneAction.java | 43 + .../ui/action/ShowDevicesAction.java | 44 + .../ui/action/StartServerAction.java | 40 + .../ui/action/StopServerAction.java | 40 + .../ui/model/ProfilesTableModel.java | 68 + .../ui/model/PropertiesTableModel.java | 92 + .../hierarchyviewer/ui/model/ViewsTreeModel.java | 62 + .../hierarchyviewer/ui/util/IconLoader.java | 49 + .../hierarchyviewer/ui/util/PngFileFilter.java | 32 + .../src/com/android/hierarchyviewer/util/OS.java | 51 + .../android/hierarchyviewer/util/WorkerThread.java | 32 + .../resources/images/icon-graph-view-selected.png | Bin 0 -> 749 bytes .../src/resources/images/icon-graph-view.png | Bin 0 -> 747 bytes .../images/icon-pixel-perfect-view-selected.png | Bin 0 -> 734 bytes .../resources/images/icon-pixel-perfect-view.png | Bin 0 -> 733 bytes jarutils/.classpath | 7 + jarutils/.gitignore | 2 + jarutils/.project | 17 + jarutils/Android.mk | 4 + jarutils/src/Android.mk | 14 + .../src/com/android/jarutils/DebugKeyProvider.java | 202 ++ .../com/android/jarutils/JavaResourceFilter.java | 103 + .../src/com/android/jarutils/KeystoreHelper.java | 228 +++ .../src/com/android/jarutils/SignedJarBuilder.java | 324 ++++ layoutlib_api/.classpath | 7 + layoutlib_api/.gitignore | 2 + layoutlib_api/.project | 17 + layoutlib_api/Android.mk | 26 + .../layoutlib/api/IDensityBasedResourceValue.java | 47 + .../com/android/layoutlib/api/ILayoutBridge.java | 212 +++ .../src/com/android/layoutlib/api/ILayoutLog.java | 42 + .../com/android/layoutlib/api/ILayoutResult.java | 101 + .../android/layoutlib/api/IProjectCallback.java | 74 + .../com/android/layoutlib/api/IResourceValue.java | 44 + .../android/layoutlib/api/IStyleResourceValue.java | 35 + .../com/android/layoutlib/api/IXmlPullParser.java | 36 + layoutlib_utils/.classpath | 7 + layoutlib_utils/.gitignore | 2 + layoutlib_utils/.project | 17 + layoutlib_utils/Android.mk | 26 + .../layoutlib/utils/DensityBasedResourceValue.java | 34 + .../com/android/layoutlib/utils/ResourceValue.java | 63 + .../layoutlib/utils/StyleResourceValue.java | 60 + .../layoutlib/utils/ValueResourceParser.java | 227 +++ layoutopt/Android.mk | 5 + layoutopt/MODULE_LICENSE_APACHE2 | 0 layoutopt/app/Android.mk | 5 + layoutopt/app/README | 3 + layoutopt/app/etc/Android.mk | 8 + layoutopt/app/etc/layoutopt | 63 + layoutopt/app/etc/layoutopt.bat | 53 + layoutopt/app/etc/manifest.txt | 2 + layoutopt/app/src/Android.mk | 15 + .../app/src/com/android/layoutopt/cli/Main.java | 123 ++ layoutopt/libs/Android.mk | 5 + layoutopt/libs/uix/Android.mk | 4 + layoutopt/libs/uix/src/Android.mk | 13 + .../com/android/layoutopt/uix/LayoutAnalysis.java | 200 ++ .../com/android/layoutopt/uix/LayoutAnalyzer.java | 249 +++ .../uix/groovy/LayoutAnalysisCategory.java | 212 +++ .../android/layoutopt/uix/rules/GroovyRule.java | 67 + .../src/com/android/layoutopt/uix/rules/Rule.java | 41 + .../android/layoutopt/uix/util/IOUtilities.java | 47 + .../layoutopt/uix/xml/XmlDocumentBuilder.java | 189 ++ .../rules/IncorrectHeightInScrollView.rule | 11 + .../IncorrectWidthInHorizontalScrollView.rule | 11 + .../uix/src/resources/rules/InefficientWeight.rule | 19 + .../src/resources/rules/MergeRootFrameLayout.rule | 18 + .../resources/rules/NestedScrollingWidgets.rule | 19 + .../uix/src/resources/rules/TooManyChildren.rule | 15 + .../uix/src/resources/rules/TooManyLevels.rule | 11 + .../libs/uix/src/resources/rules/TooManyViews.rule | 11 + .../src/resources/rules/UseCompoundDrawables.rule | 15 + .../uix/src/resources/rules/UselessLayout.rule | 19 + .../libs/uix/src/resources/rules/UselessView.rule | 14 + layoutopt/samples/compound.xml | 17 + layoutopt/samples/has_children.xml | 13 + layoutopt/samples/inefficient_weight.xml | 44 + layoutopt/samples/scrolling.xml | 19 + layoutopt/samples/simple.xml | 7 + layoutopt/samples/too_deep.xml | 85 + layoutopt/samples/too_many.xml | 413 +++++ layoutopt/samples/useless.xml | 19 + layoutopt/samples/wrong_dimension.xml | 13 + ninepatch/.classpath | 6 + ninepatch/.gitignore | 1 + ninepatch/.project | 17 + ninepatch/Android.mk | 23 + .../com/android/ninepatch/GraphicsUtilities.java | 102 + ninepatch/src/com/android/ninepatch/NinePatch.java | 514 +++++ screenshot/.classpath | 7 + screenshot/.gitignore | 1 + screenshot/.project | 17 + screenshot/Android.mk | 18 + screenshot/etc/Android.mk | 21 + screenshot/etc/manifest.txt | 1 + screenshot/etc/screenshot2 | 74 + screenshot/src/Android.mk | 27 + .../src/com/android/screenshot/Screenshot.java | 255 +++ sdklauncher/.gitignore | 1 + sdklauncher/Android.mk | 43 + sdklauncher/images/android_icon.ico | Bin 0 -> 300318 bytes sdklauncher/images/android_icon.rc | 3 + sdklauncher/sdklauncher.c | 235 +++ sdkmanager/.gitignore | 4 + sdkmanager/Android.mk | 18 + sdkmanager/MODULE_LICENSE_APACHE2 | 0 sdkmanager/app/.classpath | 12 + sdkmanager/app/.project | 19 + sdkmanager/app/Android.mk | 5 + sdkmanager/app/etc/Android.mk | 8 + sdkmanager/app/etc/android | 119 ++ sdkmanager/app/etc/android.bat | 86 + sdkmanager/app/etc/manifest.txt | 1 + sdkmanager/app/src/Android.mk | 22 + .../android/sdkmanager/CommandLineProcessor.java | 832 +++++++++ .../app/src/com/android/sdkmanager/Main.java | 1196 ++++++++++++ .../src/com/android/sdkmanager/SdkCommandLine.java | 316 ++++ .../sdkmanager/internal/repository/AboutPage.java | 112 ++ .../internal/repository/SettingsPage.java | 186 ++ .../sdkmanager/internal/repository/logo.png | Bin 0 -> 2381 bytes .../sdkmanager/CommandLineProcessorTest.java | 186 ++ .../com/android/sdkmanager/MockStdLogger.java | 48 + .../com/android/sdkmanager/SdkCommandLineTest.java | 141 ++ sdkmanager/libs/Android.mk | 18 + sdkmanager/libs/sdklib/.classpath | 10 + sdkmanager/libs/sdklib/.project | 17 + sdkmanager/libs/sdklib/Android.mk | 17 + sdkmanager/libs/sdklib/src/Android.mk | 29 + .../sdklib/src/com/android/sdklib/AddOnTarget.java | 330 ++++ .../src/com/android/sdklib/AndroidVersion.java | 288 +++ .../src/com/android/sdklib/IAndroidTarget.java | 201 ++ .../sdklib/src/com/android/sdklib/ISdkLog.java | 67 + .../sdklib/src/com/android/sdklib/NullSdkLog.java | 44 + .../src/com/android/sdklib/PlatformTarget.java | 273 +++ .../src/com/android/sdklib/SdkConstants.java | 313 ++++ .../sdklib/src/com/android/sdklib/SdkManager.java | 731 ++++++++ .../android/sdklib/internal/avd/AvdManager.java | 1482 +++++++++++++++ .../sdklib/internal/avd/HardwareProperties.java | 180 ++ .../internal/project/ApkConfigurationHelper.java | 48 + .../sdklib/internal/project/ApkSettings.java | 58 + .../sdklib/internal/project/ProjectCreator.java | 1058 +++++++++++ .../sdklib/internal/project/ProjectProperties.java | 276 +++ .../sdklib/internal/repository/AddonPackage.java | 312 ++++ .../sdklib/internal/repository/Archive.java | 1086 +++++++++++ .../sdklib/internal/repository/DocPackage.java | 216 +++ .../sdklib/internal/repository/ExtraPackage.java | 233 +++ .../sdklib/internal/repository/IDescription.java | 40 + .../repository/IMinApiLevelDependency.java | 41 + .../internal/repository/IMinToolsDependency.java | 42 + .../internal/repository/IPackageVersion.java | 36 + .../internal/repository/IPlatformDependency.java | 37 + .../android/sdklib/internal/repository/ITask.java | 26 + .../sdklib/internal/repository/ITaskFactory.java | 25 + .../sdklib/internal/repository/ITaskMonitor.java | 97 + .../sdklib/internal/repository/LocalSdkParser.java | 330 ++++ .../internal/repository/MinToolsPackage.java | 96 + .../sdklib/internal/repository/Package.java | 530 ++++++ .../internal/repository/PlatformPackage.java | 215 +++ .../sdklib/internal/repository/RepoSource.java | 798 ++++++++ .../sdklib/internal/repository/RepoSources.java | 180 ++ .../sdklib/internal/repository/SamplePackage.java | 469 +++++ .../sdklib/internal/repository/ToolPackage.java | 255 +++ .../sdklib/internal/repository/XmlParserUtils.java | 135 ++ .../android/sdklib/repository/SdkRepository.java | 156 ++ .../android/sdklib/repository/sdk-repository-1.xsd | 382 ++++ .../android/sdklib/repository/sdk-repository-2.xsd | 437 +++++ .../com/android/sdklib/xml/AndroidManifest.java | 80 + .../android/sdklib/xml/AndroidXPathFactory.java | 110 ++ .../internal/repository/MockAddonPackage.java | 136 ++ .../internal/repository/MockPlatformPackage.java | 169 ++ .../internal/repository/MockToolPackage.java | 48 + .../sdklib/internal/repository/RepoSourceTest.java | 163 ++ .../sdklib/repository/SdkRepositoryTest.java | 322 ++++ .../sdklib/testdata/repository_sample_1.xml | 275 +++ .../sdklib/testdata/repository_sample_2.xml | 310 ++++ sdkmanager/libs/sdkuilib/.classpath | 11 + sdkmanager/libs/sdkuilib/.project | 17 + sdkmanager/libs/sdkuilib/Android.mk | 4 + sdkmanager/libs/sdkuilib/README | 11 + sdkmanager/libs/sdkuilib/src/Android.mk | 20 + .../sdkuilib/internal/repository/AdbWrapper.java | 221 +++ .../sdkuilib/internal/repository/ArchiveInfo.java | 180 ++ .../internal/repository/AvdManagerPage.java | 90 + .../internal/repository/ISettingsPage.java | 87 + .../internal/repository/LocalPackagesPage.java | 319 ++++ .../internal/repository/LocalSdkAdapter.java | 108 ++ .../internal/repository/RemotePackagesPage.java | 447 +++++ .../internal/repository/RepoSourcesAdapter.java | 327 ++++ .../internal/repository/SettingsController.java | 273 +++ .../internal/repository/UpdateChooserDialog.java | 731 ++++++++ .../sdkuilib/internal/repository/UpdaterData.java | 537 ++++++ .../sdkuilib/internal/repository/UpdaterLogic.java | 845 +++++++++ .../internal/repository/UpdaterWindowImpl.java | 443 +++++ .../internal/repository/icons/ImageFactory.java | 135 ++ .../internal/repository/icons/accept_icon16.png | Bin 0 -> 253 bytes .../internal/repository/icons/addon_pkg_16.png | Bin 0 -> 539 bytes .../internal/repository/icons/android_icon_128.png | Bin 0 -> 6763 bytes .../internal/repository/icons/android_icon_16.png | Bin 0 -> 219 bytes .../internal/repository/icons/archive_icon16.png | Bin 0 -> 493 bytes .../internal/repository/icons/doc_pkg_16.png | Bin 0 -> 296 bytes .../internal/repository/icons/error_icon16.png | Bin 0 -> 626 bytes .../internal/repository/icons/extra_pkg_16.png | Bin 0 -> 428 bytes .../internal/repository/icons/incompat_icon16.png | Bin 0 -> 735 bytes .../internal/repository/icons/nopkg_icon16.png | Bin 0 -> 397 bytes .../internal/repository/icons/platform_pkg_16.png | Bin 0 -> 460 bytes .../internal/repository/icons/reject_icon16.png | Bin 0 -> 317 bytes .../internal/repository/icons/sample_pkg_16.png | Bin 0 -> 433 bytes .../internal/repository/icons/source_icon16.png | Bin 0 -> 879 bytes .../internal/repository/icons/tool_pkg_16.png | Bin 0 -> 188 bytes .../internal/repository/icons/unknown_icon16.png | Bin 0 -> 265 bytes .../sdkuilib/internal/tasks/ProgressDialog.java | 411 ++++ .../sdkuilib/internal/tasks/ProgressTask.java | 265 +++ .../internal/tasks/ProgressTaskFactory.java | 39 + .../internal/widgets/AvdCreationDialog.java | 951 ++++++++++ .../internal/widgets/AvdDetailsDialog.java | 211 +++ .../sdkuilib/internal/widgets/AvdSelector.java | 1113 +++++++++++ .../sdkuilib/internal/widgets/AvdStartDialog.java | 523 ++++++ .../internal/widgets/HardwarePropertyChooser.java | 119 ++ .../internal/widgets/ResolutionChooserDialog.java | 123 ++ .../internal/widgets/SdkTargetSelector.java | 429 +++++ .../android/sdkuilib/repository/UpdaterWindow.java | 112 ++ .../src/com/android/sdkuilib/ui/GridDialog.java | 78 + .../internal/repository/UpdaterLogicTest.java | 123 ++ sdkstats/.classpath | 8 + sdkstats/.gitignore | 2 + sdkstats/.project | 17 + sdkstats/Android.mk | 4 + sdkstats/README | 11 + sdkstats/src/Android.mk | 15 + .../src/com/android/sdkstats/SdkStatsService.java | 478 +++++ templates/AndroidManifest.alias.template | 19 + templates/AndroidManifest.template | 15 + templates/AndroidManifest.tests.template | 21 + templates/alias.template | 9 + templates/build.alias.template | 23 + templates/build.template | 67 + templates/icon_hdpi.png | Bin 0 -> 4147 bytes templates/icon_ldpi.png | Bin 0 -> 1723 bytes templates/icon_mdpi.png | Bin 0 -> 2574 bytes templates/java_file.template | 15 + templates/java_tests_file.template | 21 + templates/layout.template | 13 + templates/strings.template | 4 + traceview/.classpath | 8 + traceview/.gitignore | 1 + traceview/.project | 17 + traceview/Android.mk | 5 + traceview/README | 11 + traceview/etc/Android.mk | 8 + traceview/etc/manifest.txt | 2 + traceview/etc/traceview | 104 ++ traceview/etc/traceview.bat | 63 + traceview/src/Android.mk | 19 + traceview/src/com/android/traceview/Call.java | 141 ++ .../src/com/android/traceview/ColorController.java | 113 ++ .../src/com/android/traceview/DmTraceReader.java | 602 ++++++ .../src/com/android/traceview/MainWindow.java | 226 +++ .../src/com/android/traceview/MethodData.java | 458 +++++ .../src/com/android/traceview/ProfileData.java | 81 + .../src/com/android/traceview/ProfileNode.java | 51 + .../src/com/android/traceview/ProfileProvider.java | 361 ++++ .../src/com/android/traceview/ProfileSelf.java | 34 + .../src/com/android/traceview/ProfileView.java | 308 +++ .../src/com/android/traceview/QtraceReader.java | 45 + traceview/src/com/android/traceview/Selection.java | 70 + .../com/android/traceview/SelectionController.java | 35 + .../src/com/android/traceview/ThreadData.java | 228 +++ .../src/com/android/traceview/TickScaler.java | 148 ++ .../src/com/android/traceview/TimeLineView.java | 1961 ++++++++++++++++++++ .../src/com/android/traceview/TraceReader.java | 55 + .../src/com/android/traceview/TraceUnits.java | 93 + traceview/src/resources/icons/sort_down.png | Bin 0 -> 299 bytes traceview/src/resources/icons/sort_up.png | Bin 0 -> 292 bytes 1229 files changed, 200978 insertions(+) create mode 100644 .gitignore create mode 100644 androidprefs/.classpath create mode 100644 androidprefs/.gitignore create mode 100644 androidprefs/.project create mode 100644 androidprefs/Android.mk create mode 100644 androidprefs/src/Android.mk create mode 100644 androidprefs/src/com/android/prefs/AndroidLocation.java create mode 100644 anttasks/.classpath create mode 100644 anttasks/.gitignore create mode 100644 anttasks/.project create mode 100644 anttasks/Android.mk create mode 100644 anttasks/src/Android.mk create mode 100644 anttasks/src/com/android/ant/AaptExecLoopTask.java create mode 100644 anttasks/src/com/android/ant/ApkBuilderTask.java create mode 100644 anttasks/src/com/android/ant/SetupTask.java create mode 100644 anttasks/src/com/android/ant/XPathTask.java create mode 100644 apkbuilder/.classpath create mode 100644 apkbuilder/.gitignore create mode 100644 apkbuilder/.project create mode 100644 apkbuilder/Android.mk create mode 100644 apkbuilder/etc/Android.mk create mode 100755 apkbuilder/etc/apkbuilder create mode 100755 apkbuilder/etc/apkbuilder.bat create mode 100644 apkbuilder/etc/manifest.txt create mode 100644 apkbuilder/src/Android.mk create mode 100644 apkbuilder/src/com/android/apkbuilder/ApkBuilder.java create mode 100644 apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java create mode 100644 archquery/.classpath create mode 100644 archquery/.gitignore create mode 100644 archquery/.project create mode 100644 archquery/Android.mk create mode 100644 archquery/etc/manifest.txt create mode 100644 archquery/src/Android.mk create mode 100644 archquery/src/com/android/archquery/Main.java create mode 100644 changes.txt create mode 100644 ddms/.gitignore create mode 100644 ddms/Android.mk create mode 100644 ddms/MODULE_LICENSE_APACHE2 create mode 100644 ddms/app/.classpath create mode 100644 ddms/app/.project create mode 100644 ddms/app/Android.mk create mode 100644 ddms/app/README create mode 100644 ddms/app/etc/Android.mk create mode 100755 ddms/app/etc/ddms create mode 100755 ddms/app/etc/ddms.bat create mode 100644 ddms/app/etc/manifest.txt create mode 100644 ddms/app/src/Android.mk create mode 100644 ddms/app/src/com/android/ddms/AboutDialog.java create mode 100644 ddms/app/src/com/android/ddms/DebugPortProvider.java create mode 100644 ddms/app/src/com/android/ddms/DeviceCommandDialog.java create mode 100644 ddms/app/src/com/android/ddms/DropdownSelectionListener.java create mode 100644 ddms/app/src/com/android/ddms/Main.java create mode 100644 ddms/app/src/com/android/ddms/PrefsDialog.java create mode 100644 ddms/app/src/com/android/ddms/StaticPortConfigDialog.java create mode 100644 ddms/app/src/com/android/ddms/StaticPortEditDialog.java create mode 100644 ddms/app/src/com/android/ddms/UIThread.java create mode 100644 ddms/app/src/resources/images/ddms-icon.png create mode 100644 ddms/app/src/resources/images/ddms-logo.png create mode 100644 ddms/libs/Android.mk create mode 100644 ddms/libs/ddmlib/.classpath create mode 100644 ddms/libs/ddmlib/.project create mode 100644 ddms/libs/ddmlib/Android.mk create mode 100644 ddms/libs/ddmlib/src/Android.mk create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/Client.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/DdmConstants.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/Device.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleProfiling.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java create mode 100755 ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/Log.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java create mode 100755 ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java create mode 100644 ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java create mode 100644 ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java create mode 100644 ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java create mode 100644 ddms/libs/ddmuilib/.classpath create mode 100644 ddms/libs/ddmuilib/.project create mode 100644 ddms/libs/ddmuilib/Android.mk create mode 100644 ddms/libs/ddmuilib/README create mode 100644 ddms/libs/ddmuilib/src/Android.mk create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/SyncProgressMonitor.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/handler/BaseFileHandler.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/handler/MethodProfilingHandler.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java create mode 100644 ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java create mode 100644 ddms/libs/ddmuilib/src/resources/images/add.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/android.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/backward.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/clear.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/d.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/debug-attach.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/debug-error.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/debug-wait.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/delete.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/device.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/down.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/e.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/edit.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/empty.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/emulator.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/file.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/folder.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/forward.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/gc.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/halt.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/heap.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/hprof.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/i.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/importBug.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/load.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/pause.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/play.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/pull.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/push.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/save.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/thread.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/tracing_start.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/tracing_stop.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/up.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/v.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/w.png create mode 100644 ddms/libs/ddmuilib/src/resources/images/warning.png create mode 100644 draw9patch/Android.mk create mode 100644 draw9patch/MODULE_LICENSE_APACHE2 create mode 100644 draw9patch/etc/Android.mk create mode 100755 draw9patch/etc/draw9patch create mode 100755 draw9patch/etc/draw9patch.bat create mode 100644 draw9patch/etc/manifest.txt create mode 100644 draw9patch/src/Android.mk create mode 100644 draw9patch/src/com/android/draw9patch/Application.java create mode 100644 draw9patch/src/com/android/draw9patch/graphics/GraphicsUtilities.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/GradientPanel.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/ImageEditorPanel.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/ImageTransferHandler.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/MainFrame.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/OpenFilePanel.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/PngFileFilter.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/action/BackgroundAction.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/action/ExitAction.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/action/OpenAction.java create mode 100644 draw9patch/src/com/android/draw9patch/ui/action/SaveAction.java create mode 100644 draw9patch/src/resources/images/checker.png create mode 100644 draw9patch/src/resources/images/drop.png create mode 100644 dumpeventlog/.classpath create mode 100644 dumpeventlog/.project create mode 100644 dumpeventlog/Android.mk create mode 100644 dumpeventlog/etc/Android.mk create mode 100755 dumpeventlog/etc/dumpeventlog create mode 100644 dumpeventlog/etc/manifest.txt create mode 100644 dumpeventlog/src/Android.mk create mode 100644 dumpeventlog/src/com/android/dumpeventlog/DumpEventLog.java create mode 100644 eclipse/README_WINDOWS.txt create mode 100644 eclipse/buildConfig/allElements.xml create mode 100644 eclipse/buildConfig/build.properties create mode 100644 eclipse/buildConfig/buildUpdateSite.xml create mode 100644 eclipse/buildConfig/customTargets.xml create mode 100644 eclipse/changes.txt create mode 100644 eclipse/features/com.android.ide.eclipse.adt/.project create mode 100644 eclipse/features/com.android.ide.eclipse.adt/build.properties create mode 100644 eclipse/features/com.android.ide.eclipse.adt/feature.xml create mode 100644 eclipse/features/com.android.ide.eclipse.ddms/.project create mode 100644 eclipse/features/com.android.ide.eclipse.ddms/build.properties create mode 100644 eclipse/features/com.android.ide.eclipse.ddms/feature.xml create mode 100644 eclipse/features/com.android.ide.eclipse.tests/.project create mode 100644 eclipse/features/com.android.ide.eclipse.tests/build.properties create mode 100644 eclipse/features/com.android.ide.eclipse.tests/feature.xml create mode 100644 eclipse/plugins/.gitignore create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/.classpath create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/.gitignore create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/.project create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/NOTICE create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/about.ini create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/build.properties create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.AbsoluteLayout.groovy create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.ListView.groovy create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/android.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/android_32x32.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/android_app.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/android_file.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/android_large.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/android_project.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/androidjunit.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/avd_manager.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/az_sort.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/clipping.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/delete.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/down.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/error.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/keyboard.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/match.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/BaseViewRule.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/DropZone.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INodeProxy.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Point.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Rect.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/VersionCheck.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/ConvertToAndroidAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkDeltaVisitor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BaseBuilder.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerBuilder.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerDeltaVisitor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ResourceManagerBuilder.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidSourceViewerConfig.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/FirstElementParser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/AttributeDescriptorLabelProvider.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/BooleanAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DocumentDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/EnumAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/FlagAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/IDescriptorProvider.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ListAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/SeparatorAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/TextValueDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/XmlnsAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/BasePullParser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ExplodedRenderingHelper.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/IGraphicalLayoutEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutConstants.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutSourceViewerConfig.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/MatchingStrategy.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/WidgetPullParser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigEditDialog.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigManagerDialog.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LayoutCreatorDialog.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle1/GraphicalLayoutEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle1/PaletteFactory.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle1/UiContentOutlinePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle1/UiPropertySheetPage.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasDropListener.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasSelection.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ElementDescTransfer.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/DropFeedback.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/ElementCreateCommand.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/ElementFigure.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/LayoutFigure.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiDocumentEditPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiDocumentTreeEditPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiElementEditPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiElementTreeEditPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiElementTreeEditPartFactory.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiElementsEditPartFactory.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiLayoutEditPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiLayoutTreeEditPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiViewEditPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/parts/UiViewTreeEditPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditorContributor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestSourceViewerConfig.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ApplicationAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ClassAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestElementDescriptor.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestPkgAttrDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PackageAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostActivityCreationAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostReceiverCreationAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ThemeAttributeDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestElementNode.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestPkgAttrNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiPackageAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationAttributesPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationToggle.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/InstrumentationPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewExportPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewInfoPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewLinksPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/PermissionPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuContentAssist.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuSourceViewerConfig.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/MenuTreePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/menu/descriptors/MenuDescriptors.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/ResourcesContentAssist.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/ResourcesEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/ResourcesSourceViewerConfig.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/ResourcesTreePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/descriptors/ColorValueDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/descriptors/ItemElementDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/descriptors/ResourcesDescriptors.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/uimodel/UiColorValueNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/resources/uimodel/UiItemElementNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiSettableAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/IUiUpdateListener.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAbstractTextAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiDocumentNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiFlagAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiListAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiSeparatorAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextAttributeNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiTextValueNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlContentAssist.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlEditor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlSourceViewerConfig.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/XmlTreePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/descriptors/XmlDescriptors.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AMReceiver.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ActivityLaunchAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunch.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchConfiguration.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DelayedLaunchInfo.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DeviceChooserDialog.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmptyLaunchAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmulatorConfigTab.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/IAndroidLaunchAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ILaunchController.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/JUnitLaunchConfigDelegate.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigTabGroup.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchShortcut.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/MainLaunchConfigTab.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AndroidPreferencePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/BuildPreferencePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LaunchPreferencePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/Messages.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/UsagePreferencePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/messages.properties create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainer.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidManifestParser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ApkInstallManager.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/BaseProjectHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FixLaunchConfig.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FolderDecorator.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectChooserHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/XmlErrorHandler.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringWizard.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/XmlStringFileHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/AttrsXmlParser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/DeclareStyleableInfo.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/IIdResourceItem.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/IResourceRepository.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceItem.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceType.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ViewClassInfo.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/CountryCodeQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/KeyboardStateQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/LanguageQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/NavigationMethodQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/NetworkCodeQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/PixelDensityQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/RegionQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ResourceQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ScreenDimensionQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ScreenOrientationQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ScreenRatioQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ScreenSizeQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/TextInputMethodQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/TouchScreenQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/VersionQualifier.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ConfigurableResourceItem.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/FolderTypeRelationship.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IdResourceItem.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/IntArrayWrapper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/MultiResourceFile.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectClassLoader.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResourceItem.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/Resource.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceFile.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceFolder.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceFolderType.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/SingleResourceFile.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/files/FileWrapper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/files/FolderWrapper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/files/IAbstractFile.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/files/IAbstractFolder.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/files/IAbstractResource.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/files/IFileWrapper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/files/IFolderWrapper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoader.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidTargetData.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidTargetParser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/DexWrapper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/FrameworkResourceRepository.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/IAndroidClassLoader.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LayoutDevice.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LayoutDeviceHandler.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LayoutDeviceManager.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LayoutDevicesXsd.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LayoutParamsParser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LoadStatus.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/WidgetClassLoader.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/layout-devices.xsd create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ConfigurationSelector.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/EclipseUiHelper.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/IUpdateWizardDialog.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ReferenceChooserDialog.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceChooser.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceContentProvider.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceExplorerView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceLabelProvider.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/WizardDialogEx.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/AvdManagerAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportWizardAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewProjectAction.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewTestProjectAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewXmlFileAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/OpenWizardAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCreationPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeySelectionPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeystoreSelectionPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ProjectCheckPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectCreationPage.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/AndroidManifest.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/activity.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/icon_hdpi.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/icon_ldpi.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/icon_mdpi.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/java_file.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/launcher_intent_filter.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/preference_intent_filter.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/string.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/strings.template create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/templates/test_instrumentation.template create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/templates/test_uses-library.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/templates/uses-sdk.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/.classpath create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/.project create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/META-INF/MANIFEST.MF create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/MODULE_LICENSE_APACHE2 create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/build.properties create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/icons/.gitignore create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/icons/android.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/icons/capture.png create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/libs/.gitignore create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/.gitignore create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/CommonAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/DdmsPlugin.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/ImageLoader.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/Perspective.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferenceInitializer.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferencePage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/AllocTrackerView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/DeviceView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EmulatorControlView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EventLogView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/FileExplorerView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/HeapView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/NativeHeapView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/SelectionDependentViewPart.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/TableView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/ThreadView.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/.classpath create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/.gitignore create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/.project create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/MODULE_LICENSE_EPL create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/NOTICE create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/README.txt create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/build.properties create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class1.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class2.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/prefs.template create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/launch/JUnitLaunchConfigDelegateTest.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/sdk/TestLayoutDevicesXsd.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/sdk/config_sample.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubProjectCreationPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubProjectWizard.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AllTests.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AndroidTestPlugin.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTests.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/GroovyTestsSuite.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/SdkTestCase.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/layoutRendering/ApiDemosRenderingTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/AndroidManifestWriter.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/groovytests/TestGroovy.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/groovytests/compile_error.groovy create mode 100755 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/groovytests/invalid_interface.groovy create mode 100755 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/groovytests/simple_test.groovy create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/test.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittest.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/build/BaseBuilderTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtilsTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/ExplodeRenderingHelperTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParserTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiElementNodeTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/mock/MockNamedNodeMap.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/mock/MockNodeList.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/mock/MockXmlNode.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/CountryCodeQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/KeyboardStateQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/LanguageQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/NavigationMethodQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/NetworkCodeQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/PixelDensityQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/RegionQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/ScreenDimensionQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/ScreenOrientationQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/TextInputMethodQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/configurations/TouchScreenQualifierTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/manager/ConfigMatchTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/resources/manager/QualifierListTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/project/AndroidManifestParserTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/project/ProjectHelperTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/resources/AttrsXmlParserTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoaderTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/sdk/LayoutParamsParserTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ClasspathEntryMock.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/JavaProjectMock.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/testdata/AndroidManifest-instrumentation.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/testdata/AndroidManifest-testapp.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/testdata/jar_example.jar create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/testdata/jar_example.jardesc create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/testdata/mock_attrs.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/View.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/ViewGroup.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/LinearLayout.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/TableLayout.java create mode 100755 eclipse/scripts/_mk_icons.sh create mode 100755 eclipse/scripts/build_plugins.sh create mode 100755 eclipse/scripts/build_server.sh create mode 100755 eclipse/scripts/build_update_site.sh create mode 100755 eclipse/scripts/collect_sources_for_sdk.py create mode 100755 eclipse/scripts/create_adt_symlinks.sh create mode 100755 eclipse/scripts/create_all_symlinks.sh create mode 100755 eclipse/scripts/create_bridge_symlinks.sh create mode 100755 eclipse/scripts/create_ddms_symlinks.sh create mode 100755 eclipse/scripts/create_test_symlinks.sh create mode 100755 eclipse/scripts/gen_icon.py create mode 100755 eclipse/scripts/setup_eclipse.sh create mode 100644 eclipse/scripts/update_version.sh create mode 100644 eclipse/sites/external/.project create mode 100644 eclipse/sites/external/index.html create mode 100644 eclipse/sites/external/site.xml create mode 100644 eclipse/sites/external/web/site.css create mode 100644 eclipse/sites/external/web/site.xsl create mode 100644 eclipse/sites/internal/.project create mode 100644 eclipse/sites/internal/index.html create mode 100644 eclipse/sites/internal/site.xml create mode 100644 eclipse/sites/internal/web/site.css create mode 100644 eclipse/sites/internal/web/site.xsl create mode 100644 emulator/keymaps/AVRCP.kl create mode 100644 emulator/keymaps/Android.mk create mode 100644 emulator/keymaps/qwerty.kcm create mode 100644 emulator/keymaps/qwerty.kl create mode 100644 emulator/keymaps/qwerty2.kcm create mode 100644 emulator/mksdcard/Android.mk create mode 100644 emulator/mksdcard/mksdcard.c create mode 100644 emulator/mksdcard/vfat-empty-32MB.img.gz create mode 100644 emulator/qemud/Android.mk create mode 100644 emulator/qemud/qemud.c create mode 100644 emulator/qtools/Android.mk create mode 100644 emulator/qtools/armdis.cpp create mode 100644 emulator/qtools/armdis.h create mode 100644 emulator/qtools/bb2sym.cpp create mode 100644 emulator/qtools/bb_dump.cpp create mode 100644 emulator/qtools/bbprof.cpp create mode 100644 emulator/qtools/bitvector.h create mode 100644 emulator/qtools/callstack.h create mode 100644 emulator/qtools/check_stack.cpp create mode 100644 emulator/qtools/check_trace.cpp create mode 100644 emulator/qtools/coverage.cpp create mode 100644 emulator/qtools/decoder.cpp create mode 100644 emulator/qtools/decoder.h create mode 100644 emulator/qtools/dmtrace.cpp create mode 100644 emulator/qtools/dmtrace.h create mode 100644 emulator/qtools/dump_regions.cpp create mode 100644 emulator/qtools/exc_dump.cpp create mode 100644 emulator/qtools/gtrace.cpp create mode 100644 emulator/qtools/gtrace.h create mode 100644 emulator/qtools/hash_table.h create mode 100644 emulator/qtools/hist_trace.cpp create mode 100644 emulator/qtools/opcode.cpp create mode 100644 emulator/qtools/opcode.h create mode 100644 emulator/qtools/parse_options-inl.h create mode 100644 emulator/qtools/parse_options.cpp create mode 100644 emulator/qtools/parse_options.h create mode 100644 emulator/qtools/post_trace.cpp create mode 100644 emulator/qtools/profile_pid.cpp create mode 100644 emulator/qtools/profile_trace.cpp create mode 100644 emulator/qtools/q2dm.cpp create mode 100644 emulator/qtools/q2g.cpp create mode 100644 emulator/qtools/read_addr.cpp create mode 100644 emulator/qtools/read_elf.cpp create mode 100644 emulator/qtools/read_elf.h create mode 100644 emulator/qtools/read_method.cpp create mode 100644 emulator/qtools/read_pid.cpp create mode 100644 emulator/qtools/read_trace.cpp create mode 100644 emulator/qtools/stack_dump.cpp create mode 100644 emulator/qtools/tests/common_head.mk create mode 100644 emulator/qtools/tests/common_tail.mk create mode 100644 emulator/qtools/tests/gtrace/Makefile create mode 100644 emulator/qtools/tests/gtrace/test.c create mode 100644 emulator/qtools/tests/macros.h create mode 100644 emulator/qtools/tests/tests.ld create mode 100644 emulator/qtools/thumbdis.cpp create mode 100644 emulator/qtools/trace_reader.cpp create mode 100644 emulator/qtools/trace_reader.h create mode 100644 emulator/qtools/trace_reader_base.h create mode 100644 emulator/sensors/Android.mk create mode 100644 emulator/sensors/sensors_qemu.c create mode 100644 emulator/skins/HVGA/arrow_down.png create mode 100644 emulator/skins/HVGA/arrow_left.png create mode 100644 emulator/skins/HVGA/arrow_right.png create mode 100644 emulator/skins/HVGA/arrow_up.png create mode 100644 emulator/skins/HVGA/background_land.png create mode 100644 emulator/skins/HVGA/background_port.png create mode 100644 emulator/skins/HVGA/button.png create mode 100644 emulator/skins/HVGA/controls.png create mode 100644 emulator/skins/HVGA/hardware.ini create mode 100644 emulator/skins/HVGA/key.png create mode 100644 emulator/skins/HVGA/keyboard.png create mode 100644 emulator/skins/HVGA/layout create mode 100644 emulator/skins/HVGA/select.png create mode 100644 emulator/skins/HVGA/spacebar.png create mode 100644 emulator/skins/QVGA/arrow_down.png create mode 100644 emulator/skins/QVGA/arrow_left.png create mode 100644 emulator/skins/QVGA/arrow_right.png create mode 100644 emulator/skins/QVGA/arrow_up.png create mode 100644 emulator/skins/QVGA/background_land.png create mode 100644 emulator/skins/QVGA/background_port.png create mode 100644 emulator/skins/QVGA/button.png create mode 100644 emulator/skins/QVGA/controls.png create mode 100644 emulator/skins/QVGA/hardware.ini create mode 100644 emulator/skins/QVGA/key.png create mode 100644 emulator/skins/QVGA/keyboard.png create mode 100644 emulator/skins/QVGA/layout create mode 100644 emulator/skins/QVGA/select.png create mode 100644 emulator/skins/QVGA/spacebar.png create mode 100644 emulator/skins/WQVGA400/arrow_down.png create mode 100644 emulator/skins/WQVGA400/arrow_left.png create mode 100644 emulator/skins/WQVGA400/arrow_right.png create mode 100644 emulator/skins/WQVGA400/arrow_up.png create mode 100644 emulator/skins/WQVGA400/background_land.png create mode 100644 emulator/skins/WQVGA400/background_port.png create mode 100644 emulator/skins/WQVGA400/button.png create mode 100644 emulator/skins/WQVGA400/controls.png create mode 100644 emulator/skins/WQVGA400/hardware.ini create mode 100644 emulator/skins/WQVGA400/key.png create mode 100644 emulator/skins/WQVGA400/keyboard.png create mode 100644 emulator/skins/WQVGA400/layout create mode 100644 emulator/skins/WQVGA400/select.png create mode 100644 emulator/skins/WQVGA400/spacebar.png create mode 100644 emulator/skins/WQVGA432/arrow_down.png create mode 100644 emulator/skins/WQVGA432/arrow_left.png create mode 100644 emulator/skins/WQVGA432/arrow_right.png create mode 100644 emulator/skins/WQVGA432/arrow_up.png create mode 100644 emulator/skins/WQVGA432/background_land.png create mode 100644 emulator/skins/WQVGA432/background_port.png create mode 100644 emulator/skins/WQVGA432/button.png create mode 100644 emulator/skins/WQVGA432/controls.png create mode 100644 emulator/skins/WQVGA432/hardware.ini create mode 100644 emulator/skins/WQVGA432/key.png create mode 100644 emulator/skins/WQVGA432/keyboard.png create mode 100644 emulator/skins/WQVGA432/layout create mode 100644 emulator/skins/WQVGA432/select.png create mode 100644 emulator/skins/WQVGA432/spacebar.png create mode 100644 emulator/skins/WVGA800/arrow_down.png create mode 100644 emulator/skins/WVGA800/arrow_left.png create mode 100644 emulator/skins/WVGA800/arrow_right.png create mode 100644 emulator/skins/WVGA800/arrow_up.png create mode 100644 emulator/skins/WVGA800/background_land.png create mode 100644 emulator/skins/WVGA800/background_port.png create mode 100644 emulator/skins/WVGA800/button.png create mode 100644 emulator/skins/WVGA800/controls.png create mode 100644 emulator/skins/WVGA800/hardware.ini create mode 100644 emulator/skins/WVGA800/key.png create mode 100644 emulator/skins/WVGA800/keyboard.png create mode 100644 emulator/skins/WVGA800/layout create mode 100644 emulator/skins/WVGA800/select.png create mode 100644 emulator/skins/WVGA800/spacebar.png create mode 100644 emulator/skins/WVGA854/arrow_down.png create mode 100644 emulator/skins/WVGA854/arrow_left.png create mode 100644 emulator/skins/WVGA854/arrow_right.png create mode 100644 emulator/skins/WVGA854/arrow_up.png create mode 100644 emulator/skins/WVGA854/background_land.png create mode 100644 emulator/skins/WVGA854/background_port.png create mode 100644 emulator/skins/WVGA854/button.png create mode 100644 emulator/skins/WVGA854/controls.png create mode 100644 emulator/skins/WVGA854/hardware.ini create mode 100644 emulator/skins/WVGA854/key.png create mode 100644 emulator/skins/WVGA854/keyboard.png create mode 100644 emulator/skins/WVGA854/layout create mode 100644 emulator/skins/WVGA854/select.png create mode 100644 emulator/skins/WVGA854/spacebar.png create mode 100644 emulator/tools/Android.mk create mode 100644 emulator/tools/qemu-props.c create mode 100644 eventanalyzer/.classpath create mode 100644 eventanalyzer/.project create mode 100644 eventanalyzer/Android.mk create mode 100644 eventanalyzer/etc/Android.mk create mode 100755 eventanalyzer/etc/eventanalyzer create mode 100644 eventanalyzer/etc/manifest.txt create mode 100644 eventanalyzer/src/Android.mk create mode 100644 eventanalyzer/src/com/android/eventanalyzer/EventAnalyzer.java create mode 100644 files/README_add-ons.txt create mode 100644 files/alias_rules.xml create mode 100644 files/android.el create mode 100644 files/android_rules.xml create mode 100644 files/android_test_rules.xml create mode 100644 files/devices.xml create mode 100755 files/find_java.bat create mode 100644 files/plugin.prop create mode 100644 files/post_tools_install.bat create mode 100644 files/sdk_files_NOTICE.txt create mode 100644 files/tools_source.properties create mode 100644 hierarchyviewer/Android.mk create mode 100644 hierarchyviewer/MODULE_LICENSE_APACHE2 create mode 100644 hierarchyviewer/etc/Android.mk create mode 100755 hierarchyviewer/etc/hierarchyviewer create mode 100755 hierarchyviewer/etc/hierarchyviewer.bat create mode 100644 hierarchyviewer/etc/manifest.txt create mode 100644 hierarchyviewer/src/Android.mk create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/HierarchyViewer.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/device/Configuration.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/device/DeviceBridge.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/device/Window.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/laf/UnifiedContentBorder.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/scene/CaptureLoader.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/scene/ProfilesLoader.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/scene/WindowsLoader.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/CaptureRenderer.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/BackgroundAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureNodeAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ExitAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/InvalidateAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/LoadGraphAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RefreshWindowsAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RequestLayoutAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/SaveSceneAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ShowDevicesAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StartServerAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StopServerAction.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ProfilesTableModel.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/model/PropertiesTableModel.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ViewsTreeModel.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/util/IconLoader.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PngFileFilter.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/util/OS.java create mode 100644 hierarchyviewer/src/com/android/hierarchyviewer/util/WorkerThread.java create mode 100644 hierarchyviewer/src/resources/images/icon-graph-view-selected.png create mode 100644 hierarchyviewer/src/resources/images/icon-graph-view.png create mode 100644 hierarchyviewer/src/resources/images/icon-pixel-perfect-view-selected.png create mode 100644 hierarchyviewer/src/resources/images/icon-pixel-perfect-view.png create mode 100644 jarutils/.classpath create mode 100644 jarutils/.gitignore create mode 100644 jarutils/.project create mode 100644 jarutils/Android.mk create mode 100644 jarutils/src/Android.mk create mode 100644 jarutils/src/com/android/jarutils/DebugKeyProvider.java create mode 100644 jarutils/src/com/android/jarutils/JavaResourceFilter.java create mode 100644 jarutils/src/com/android/jarutils/KeystoreHelper.java create mode 100644 jarutils/src/com/android/jarutils/SignedJarBuilder.java create mode 100644 layoutlib_api/.classpath create mode 100644 layoutlib_api/.gitignore create mode 100644 layoutlib_api/.project create mode 100644 layoutlib_api/Android.mk create mode 100644 layoutlib_api/src/com/android/layoutlib/api/IDensityBasedResourceValue.java create mode 100644 layoutlib_api/src/com/android/layoutlib/api/ILayoutBridge.java create mode 100644 layoutlib_api/src/com/android/layoutlib/api/ILayoutLog.java create mode 100644 layoutlib_api/src/com/android/layoutlib/api/ILayoutResult.java create mode 100644 layoutlib_api/src/com/android/layoutlib/api/IProjectCallback.java create mode 100644 layoutlib_api/src/com/android/layoutlib/api/IResourceValue.java create mode 100644 layoutlib_api/src/com/android/layoutlib/api/IStyleResourceValue.java create mode 100644 layoutlib_api/src/com/android/layoutlib/api/IXmlPullParser.java create mode 100644 layoutlib_utils/.classpath create mode 100644 layoutlib_utils/.gitignore create mode 100644 layoutlib_utils/.project create mode 100644 layoutlib_utils/Android.mk create mode 100644 layoutlib_utils/src/com/android/layoutlib/utils/DensityBasedResourceValue.java create mode 100644 layoutlib_utils/src/com/android/layoutlib/utils/ResourceValue.java create mode 100644 layoutlib_utils/src/com/android/layoutlib/utils/StyleResourceValue.java create mode 100644 layoutlib_utils/src/com/android/layoutlib/utils/ValueResourceParser.java create mode 100644 layoutopt/Android.mk create mode 100644 layoutopt/MODULE_LICENSE_APACHE2 create mode 100644 layoutopt/app/Android.mk create mode 100644 layoutopt/app/README create mode 100644 layoutopt/app/etc/Android.mk create mode 100755 layoutopt/app/etc/layoutopt create mode 100755 layoutopt/app/etc/layoutopt.bat create mode 100644 layoutopt/app/etc/manifest.txt create mode 100644 layoutopt/app/src/Android.mk create mode 100644 layoutopt/app/src/com/android/layoutopt/cli/Main.java create mode 100644 layoutopt/libs/Android.mk create mode 100644 layoutopt/libs/uix/Android.mk create mode 100644 layoutopt/libs/uix/src/Android.mk create mode 100644 layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalysis.java create mode 100644 layoutopt/libs/uix/src/com/android/layoutopt/uix/LayoutAnalyzer.java create mode 100644 layoutopt/libs/uix/src/com/android/layoutopt/uix/groovy/LayoutAnalysisCategory.java create mode 100644 layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/GroovyRule.java create mode 100644 layoutopt/libs/uix/src/com/android/layoutopt/uix/rules/Rule.java create mode 100644 layoutopt/libs/uix/src/com/android/layoutopt/uix/util/IOUtilities.java create mode 100644 layoutopt/libs/uix/src/com/android/layoutopt/uix/xml/XmlDocumentBuilder.java create mode 100644 layoutopt/libs/uix/src/resources/rules/IncorrectHeightInScrollView.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/IncorrectWidthInHorizontalScrollView.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/InefficientWeight.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/MergeRootFrameLayout.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/NestedScrollingWidgets.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/TooManyChildren.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/TooManyLevels.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/TooManyViews.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/UseCompoundDrawables.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/UselessLayout.rule create mode 100644 layoutopt/libs/uix/src/resources/rules/UselessView.rule create mode 100644 layoutopt/samples/compound.xml create mode 100644 layoutopt/samples/has_children.xml create mode 100644 layoutopt/samples/inefficient_weight.xml create mode 100644 layoutopt/samples/scrolling.xml create mode 100644 layoutopt/samples/simple.xml create mode 100644 layoutopt/samples/too_deep.xml create mode 100644 layoutopt/samples/too_many.xml create mode 100644 layoutopt/samples/useless.xml create mode 100644 layoutopt/samples/wrong_dimension.xml create mode 100644 ninepatch/.classpath create mode 100644 ninepatch/.gitignore create mode 100644 ninepatch/.project create mode 100644 ninepatch/Android.mk create mode 100644 ninepatch/src/com/android/ninepatch/GraphicsUtilities.java create mode 100644 ninepatch/src/com/android/ninepatch/NinePatch.java create mode 100644 screenshot/.classpath create mode 100644 screenshot/.gitignore create mode 100644 screenshot/.project create mode 100644 screenshot/Android.mk create mode 100644 screenshot/etc/Android.mk create mode 100644 screenshot/etc/manifest.txt create mode 100755 screenshot/etc/screenshot2 create mode 100644 screenshot/src/Android.mk create mode 100644 screenshot/src/com/android/screenshot/Screenshot.java create mode 100644 sdklauncher/.gitignore create mode 100644 sdklauncher/Android.mk create mode 100644 sdklauncher/images/android_icon.ico create mode 100644 sdklauncher/images/android_icon.rc create mode 100644 sdklauncher/sdklauncher.c create mode 100644 sdkmanager/.gitignore create mode 100644 sdkmanager/Android.mk create mode 100644 sdkmanager/MODULE_LICENSE_APACHE2 create mode 100644 sdkmanager/app/.classpath create mode 100644 sdkmanager/app/.project create mode 100644 sdkmanager/app/Android.mk create mode 100644 sdkmanager/app/etc/Android.mk create mode 100755 sdkmanager/app/etc/android create mode 100755 sdkmanager/app/etc/android.bat create mode 100644 sdkmanager/app/etc/manifest.txt create mode 100644 sdkmanager/app/src/Android.mk create mode 100644 sdkmanager/app/src/com/android/sdkmanager/CommandLineProcessor.java create mode 100644 sdkmanager/app/src/com/android/sdkmanager/Main.java create mode 100644 sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java create mode 100755 sdkmanager/app/src/com/android/sdkmanager/internal/repository/AboutPage.java create mode 100755 sdkmanager/app/src/com/android/sdkmanager/internal/repository/SettingsPage.java create mode 100644 sdkmanager/app/src/com/android/sdkmanager/internal/repository/logo.png create mode 100644 sdkmanager/app/tests/com/android/sdkmanager/CommandLineProcessorTest.java create mode 100644 sdkmanager/app/tests/com/android/sdkmanager/MockStdLogger.java create mode 100644 sdkmanager/app/tests/com/android/sdkmanager/SdkCommandLineTest.java create mode 100644 sdkmanager/libs/Android.mk create mode 100644 sdkmanager/libs/sdklib/.classpath create mode 100644 sdkmanager/libs/sdklib/.project create mode 100644 sdkmanager/libs/sdklib/Android.mk create mode 100644 sdkmanager/libs/sdklib/src/Android.mk create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/AddOnTarget.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/AndroidVersion.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/IAndroidTarget.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/ISdkLog.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/NullSdkLog.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/HardwareProperties.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ApkConfigurationHelper.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ApkSettings.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectCreator.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/IDescription.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/IMinApiLevelDependency.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/IMinToolsDependency.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/IPackageVersion.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/IPlatformDependency.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITask.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskFactory.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/LocalSdkParser.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/MinToolsPackage.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSource.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSources.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/XmlParserUtils.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkRepository.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-repository-1.xsd create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-repository-2.xsd create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/xml/AndroidManifest.java create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/xml/AndroidXPathFactory.java create mode 100755 sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockAddonPackage.java create mode 100755 sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockPlatformPackage.java create mode 100755 sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockToolPackage.java create mode 100755 sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/RepoSourceTest.java create mode 100755 sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/SdkRepositoryTest.java create mode 100755 sdkmanager/libs/sdklib/tests/com/android/sdklib/testdata/repository_sample_1.xml create mode 100755 sdkmanager/libs/sdklib/tests/com/android/sdklib/testdata/repository_sample_2.xml create mode 100644 sdkmanager/libs/sdkuilib/.classpath create mode 100644 sdkmanager/libs/sdkuilib/.project create mode 100644 sdkmanager/libs/sdkuilib/Android.mk create mode 100644 sdkmanager/libs/sdkuilib/README create mode 100644 sdkmanager/libs/sdkuilib/src/Android.mk create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/AdbWrapper.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ArchiveInfo.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/AvdManagerPage.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ISettingsPage.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalSdkAdapter.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RemotePackagesPage.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RepoSourcesAdapter.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SettingsController.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterLogic.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/ImageFactory.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/accept_icon16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/android_icon_128.png create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/android_icon_16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/archive_icon16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/error_icon16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/incompat_icon16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/nopkg_icon16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/reject_icon16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/sample_pkg_16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/source_icon16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/tool_pkg_16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/unknown_icon16.png create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressDialog.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTask.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/AvdSelector.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/AvdStartDialog.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java create mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/repository/UpdaterWindow.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ui/GridDialog.java create mode 100755 sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java create mode 100644 sdkstats/.classpath create mode 100644 sdkstats/.gitignore create mode 100644 sdkstats/.project create mode 100644 sdkstats/Android.mk create mode 100644 sdkstats/README create mode 100644 sdkstats/src/Android.mk create mode 100644 sdkstats/src/com/android/sdkstats/SdkStatsService.java create mode 100644 templates/AndroidManifest.alias.template create mode 100644 templates/AndroidManifest.template create mode 100644 templates/AndroidManifest.tests.template create mode 100644 templates/alias.template create mode 100644 templates/build.alias.template create mode 100644 templates/build.template create mode 100644 templates/icon_hdpi.png create mode 100644 templates/icon_ldpi.png create mode 100644 templates/icon_mdpi.png create mode 100644 templates/java_file.template create mode 100644 templates/java_tests_file.template create mode 100644 templates/layout.template create mode 100644 templates/strings.template create mode 100644 traceview/.classpath create mode 100644 traceview/.gitignore create mode 100644 traceview/.project create mode 100644 traceview/Android.mk create mode 100644 traceview/README create mode 100644 traceview/etc/Android.mk create mode 100644 traceview/etc/manifest.txt create mode 100755 traceview/etc/traceview create mode 100755 traceview/etc/traceview.bat create mode 100644 traceview/src/Android.mk create mode 100644 traceview/src/com/android/traceview/Call.java create mode 100644 traceview/src/com/android/traceview/ColorController.java create mode 100644 traceview/src/com/android/traceview/DmTraceReader.java create mode 100644 traceview/src/com/android/traceview/MainWindow.java create mode 100644 traceview/src/com/android/traceview/MethodData.java create mode 100644 traceview/src/com/android/traceview/ProfileData.java create mode 100644 traceview/src/com/android/traceview/ProfileNode.java create mode 100644 traceview/src/com/android/traceview/ProfileProvider.java create mode 100644 traceview/src/com/android/traceview/ProfileSelf.java create mode 100644 traceview/src/com/android/traceview/ProfileView.java create mode 100644 traceview/src/com/android/traceview/QtraceReader.java create mode 100644 traceview/src/com/android/traceview/Selection.java create mode 100644 traceview/src/com/android/traceview/SelectionController.java create mode 100644 traceview/src/com/android/traceview/ThreadData.java create mode 100644 traceview/src/com/android/traceview/TickScaler.java create mode 100644 traceview/src/com/android/traceview/TimeLineView.java create mode 100644 traceview/src/com/android/traceview/TraceReader.java create mode 100644 traceview/src/com/android/traceview/TraceUnits.java create mode 100644 traceview/src/resources/icons/sort_down.png create mode 100644 traceview/src/resources/icons/sort_up.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..452fd8161 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*~ +*.bak +*.pyc +Thumbs.db + diff --git a/androidprefs/.classpath b/androidprefs/.classpath new file mode 100644 index 000000000..fb5011632 --- /dev/null +++ b/androidprefs/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/androidprefs/.gitignore b/androidprefs/.gitignore new file mode 100644 index 000000000..fe99505dc --- /dev/null +++ b/androidprefs/.gitignore @@ -0,0 +1,2 @@ +bin + diff --git a/androidprefs/.project b/androidprefs/.project new file mode 100644 index 000000000..6633bba3c --- /dev/null +++ b/androidprefs/.project @@ -0,0 +1,17 @@ + + + AndroidPrefs + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/androidprefs/Android.mk b/androidprefs/Android.mk new file mode 100644 index 000000000..363b085c4 --- /dev/null +++ b/androidprefs/Android.mk @@ -0,0 +1,17 @@ +# +# 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. +# +JARUTILS_LOCAL_DIR := $(call my-dir) +include $(JARUTILS_LOCAL_DIR)/src/Android.mk diff --git a/androidprefs/src/Android.mk b/androidprefs/src/Android.mk new file mode 100644 index 000000000..ddc0aa62e --- /dev/null +++ b/androidprefs/src/Android.mk @@ -0,0 +1,24 @@ +# +# 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE := androidprefs + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/androidprefs/src/com/android/prefs/AndroidLocation.java b/androidprefs/src/com/android/prefs/AndroidLocation.java new file mode 100644 index 000000000..446c42656 --- /dev/null +++ b/androidprefs/src/com/android/prefs/AndroidLocation.java @@ -0,0 +1,108 @@ +/* + * 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.prefs; + +import java.io.File; + +/** + * Manages the location of the android files (including emulator files, ddms config, debug keystore) + */ +public final class AndroidLocation { + /** + * Virtual Device folder inside the path returned by {@link #getFolder()} + */ + public static final String FOLDER_AVD = "avd"; + + /** + * Throw when the location of the android folder couldn't be found. + */ + public static final class AndroidLocationException extends Exception { + private static final long serialVersionUID = 1L; + + public AndroidLocationException(String string) { + super(string); + } + } + + private static String sPrefsLocation = null; + + /** + * Returns the folder used to store android related files. + * @return an OS specific path, terminated by a separator. + * @throws AndroidLocationException + */ + public final static String getFolder() throws AndroidLocationException { + if (sPrefsLocation == null) { + String home = findValidPath("ANDROID_SDK_HOME", "user.home", "HOME"); + + // if the above failed, we throw an exception. + if (home == null) { + throw new AndroidLocationException( + "Unable to get the home directory. Make sure the user.home property is set up"); + } else { + sPrefsLocation = home + File.separator + ".android" + File.separator; + } + } + + // make sure the folder exists! + File f = new File(sPrefsLocation); + if (f.exists() == false) { + try { + f.mkdir(); + } catch (SecurityException e) { + AndroidLocationException e2 = new AndroidLocationException(String.format( + "Unable to create folder '%1$s'. " + + "This is the path of preference folder expected by the Android tools.", + sPrefsLocation)); + e2.initCause(e); + throw e2; + } + } else if (f.isFile()) { + throw new AndroidLocationException(sPrefsLocation + + " is not a directory! " + + "This is the path of preference folder expected by the Android tools."); + } + + return sPrefsLocation; + } + + /** + * Checks a list of system properties and/or system environment variables for validity, and + * existing director, and returns the first one. + * @param names + * @return the content of the first property/variable. + */ + private static String findValidPath(String... names) { + for (String name : names) { + String path; + if (name.indexOf('.') != -1) { + path = System.getProperty(name); + } else { + path = System.getenv(name); + } + + if (path != null) { + File f = new File(path); + if (f.isDirectory()) { + return path; + } + } + } + + return null; + } +} diff --git a/anttasks/.classpath b/anttasks/.classpath new file mode 100644 index 000000000..d6ce15a37 --- /dev/null +++ b/anttasks/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/anttasks/.gitignore b/anttasks/.gitignore new file mode 100644 index 000000000..fe99505dc --- /dev/null +++ b/anttasks/.gitignore @@ -0,0 +1,2 @@ +bin + diff --git a/anttasks/.project b/anttasks/.project new file mode 100644 index 000000000..aed1b6164 --- /dev/null +++ b/anttasks/.project @@ -0,0 +1,17 @@ + + + ant-tasks + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/anttasks/Android.mk b/anttasks/Android.mk new file mode 100644 index 000000000..15ee90361 --- /dev/null +++ b/anttasks/Android.mk @@ -0,0 +1,17 @@ +# +# 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. +# +ANTTASKS_LOCAL_DIR := $(call my-dir) +include $(ANTTASKS_LOCAL_DIR)/src/Android.mk diff --git a/anttasks/src/Android.mk b/anttasks/src/Android.mk new file mode 100644 index 000000000..94d6d3f10 --- /dev/null +++ b/anttasks/src/Android.mk @@ -0,0 +1,29 @@ +# +# 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAVA_LIBRARIES := \ + sdklib \ + apkbuilder \ + ant + +LOCAL_MODULE := anttasks + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/anttasks/src/com/android/ant/AaptExecLoopTask.java b/anttasks/src/com/android/ant/AaptExecLoopTask.java new file mode 100644 index 000000000..6610d23d6 --- /dev/null +++ b/anttasks/src/com/android/ant/AaptExecLoopTask.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2009 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.ant; + +import com.android.sdklib.internal.project.ApkConfigurationHelper; +import com.android.sdklib.internal.project.ApkSettings; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.taskdefs.ExecTask; +import org.apache.tools.ant.types.Path; + +import java.io.File; +import java.util.ArrayList; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Task able to run an Exec task on aapt several times. + * It does not follow the exec task format, instead it has its own parameters, which maps + * directly to aapt. + * + */ +public final class AaptExecLoopTask extends Task { + + /** + * Class representing a <nocompress> node in the main task XML. + * This let the developers prevent compression of some files in assets/ and res/raw/ + * by extension. + * If the extension is null, this will disable compression for all files in assets/ and + * res/raw/ + */ + public final static class NoCompress { + String mExtension; + + /** + * Sets the value of the "extension" attribute. + * @param extention the extension. + */ + public void setExtension(String extention) { + mExtension = extention; + } + } + + private String mExecutable; + private String mCommand; + private String mManifest; + private String mResources; + private String mAssets; + private String mAndroidJar; + private String mOutFolder; + private String mBaseName; + private final ArrayList mNoCompressList = new ArrayList(); + + /** + * Sets the value of the "executable" attribute. + * @param executable the value. + */ + public void setExecutable(String executable) { + mExecutable = executable; + } + + /** + * Sets the value of the "command" attribute. + * @param command the value. + */ + public void setCommand(String command) { + mCommand = command; + } + + /** + * Sets the value of the "manifest" attribute. + * @param manifest the value. + */ + public void setManifest(Path manifest) { + mManifest = manifest.toString(); + } + + /** + * Sets the value of the "resources" attribute. + * @param resources the value. + */ + public void setResources(Path resources) { + mResources = resources.toString(); + } + + /** + * Sets the value of the "assets" attribute. + * @param assets the value. + */ + public void setAssets(Path assets) { + mAssets = assets.toString(); + } + + /** + * Sets the value of the "androidjar" attribute. + * @param androidJar the value. + */ + public void setAndroidjar(Path androidJar) { + mAndroidJar = androidJar.toString(); + } + + /** + * Sets the value of the "outfolder" attribute. + * @param outFolder the value. + */ + public void setOutfolder(Path outFolder) { + mOutFolder = outFolder.toString(); + } + + /** + * Sets the value of the "basename" attribute. + * @param baseName the value. + */ + public void setBasename(String baseName) { + mBaseName = baseName; + } + + /** + * Returns an object representing a nested nocompress element. + */ + public Object createNocompress() { + NoCompress nc = new NoCompress(); + mNoCompressList.add(nc); + return nc; + } + + /* + * (non-Javadoc) + * + * Executes the loop. Based on the values inside default.properties, this will + * create alternate temporary ap_ files. + * + * @see org.apache.tools.ant.Task#execute() + */ + @Override + public void execute() throws BuildException { + Project taskProject = getProject(); + + // first do a full resource package + createPackage(null /*configName*/, null /*resourceFilter*/); + + // now see if we need to create file with filtered resources. + // Get the project base directory. + File baseDir = taskProject.getBaseDir(); + ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(), + PropertyType.DEFAULT); + + + ApkSettings apkSettings = ApkConfigurationHelper.getSettings(properties); + if (apkSettings != null) { + Map apkFilters = apkSettings.getResourceFilters(); + if (apkFilters.size() > 0) { + for (Entry entry : apkFilters.entrySet()) { + createPackage(entry.getKey(), entry.getValue()); + } + } + } + } + + /** + * Creates a resource package. + * @param configName the name of the filter config. Can be null in which case a full resource + * package will be generated. + * @param resourceFilter the resource configuration filter to pass to aapt (if configName is + * non null) + */ + private void createPackage(String configName, String resourceFilter) { + Project taskProject = getProject(); + + if (configName == null || resourceFilter == null) { + System.out.println("Creating full resource package..."); + } else { + System.out.println(String.format( + "Creating resource package for config '%1$s' (%2$s)...", + configName, resourceFilter)); + } + + // create a task for the default apk. + ExecTask task = new ExecTask(); + task.setExecutable(mExecutable); + task.setFailonerror(true); + + // aapt command. Only "package" is supported at this time really. + task.createArg().setValue(mCommand); + + // force flag + task.createArg().setValue("-f"); + + // filters if needed + if (configName != null && resourceFilter != null) { + task.createArg().setValue("-c"); + task.createArg().setValue(resourceFilter); + } + + // no compress flag + // first look to see if there's a NoCompress object with no specified extension + boolean compressNothing = false; + for (NoCompress nc : mNoCompressList) { + if (nc.mExtension == null) { + task.createArg().setValue("-0"); + task.createArg().setValue(""); + compressNothing = true; + break; + } + } + + if (compressNothing == false) { + for (NoCompress nc : mNoCompressList) { + task.createArg().setValue("-0"); + task.createArg().setValue(nc.mExtension); + } + } + + // manifest location + task.createArg().setValue("-M"); + task.createArg().setValue(mManifest); + + // resources location. This may not exists, and aapt doesn't like it, so we check first. + File res = new File(mResources); + if (res.isDirectory()) { + task.createArg().setValue("-S"); + task.createArg().setValue(mResources); + } + + // assets location. This may not exists, and aapt doesn't like it, so we check first. + File assets = new File(mAssets); + if (assets.isDirectory()) { + task.createArg().setValue("-A"); + task.createArg().setValue(mAssets); + } + + // android.jar + task.createArg().setValue("-I"); + task.createArg().setValue(mAndroidJar); + + // out file. This is based on the outFolder, baseName, and the configName (if applicable) + String filename; + if (configName != null && resourceFilter != null) { + filename = mBaseName + "-" + configName + ".ap_"; + } else { + filename = mBaseName + ".ap_"; + } + + File file = new File(mOutFolder, filename); + task.createArg().setValue("-F"); + task.createArg().setValue(file.getAbsolutePath()); + + // final setup of the task + task.setProject(taskProject); + task.setOwningTarget(getOwningTarget()); + + // execute it. + task.execute(); + } +} diff --git a/anttasks/src/com/android/ant/ApkBuilderTask.java b/anttasks/src/com/android/ant/ApkBuilderTask.java new file mode 100644 index 000000000..b1e6d80fe --- /dev/null +++ b/anttasks/src/com/android/ant/ApkBuilderTask.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2009 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.ant; + +import com.android.apkbuilder.ApkBuilder.ApkCreationException; +import com.android.apkbuilder.internal.ApkBuilderImpl; +import com.android.apkbuilder.internal.ApkBuilderImpl.ApkFile; +import com.android.sdklib.internal.project.ApkConfigurationHelper; +import com.android.sdklib.internal.project.ApkSettings; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.ProjectComponent; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.Path; +import org.apache.tools.ant.types.Path.PathElement; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Map; +import java.util.Map.Entry; + +public class ApkBuilderTask extends Task { + + // ref id to the object containing all the boot classpaths. + private final static String REF_APK_PATH = "android.apks.path"; + + /** + * Class to represent nested elements. Since they all have only one attribute ('path'), the + * same class can be used for all the nested elements (zip, file, sourcefolder, jarfolder, + * nativefolder). + */ + public final static class Value extends ProjectComponent { + String mPath; + + /** + * Sets the value of the "path" attribute. + * @param path the value. + */ + public void setPath(Path path) { + mPath = path.toString(); + } + } + + private String mOutFolder; + private String mBaseName; + private boolean mVerbose = false; + private boolean mSigned = true; + private boolean mDebug = false; + + private final ArrayList mZipList = new ArrayList(); + private final ArrayList mFileList = new ArrayList(); + private final ArrayList mSourceList = new ArrayList(); + private final ArrayList mJarfolderList = new ArrayList(); + private final ArrayList mJarfileList = new ArrayList(); + private final ArrayList mNativeList = new ArrayList(); + + private final ArrayList mZipArchives = new ArrayList(); + private final ArrayList mArchiveFiles = new ArrayList(); + private final ArrayList mJavaResources = new ArrayList(); + private final ArrayList mResourcesJars = new ArrayList(); + private final ArrayList mNativeLibraries = new ArrayList(); + + /** + * Sets the value of the "outfolder" attribute. + * @param outFolder the value. + */ + public void setOutfolder(Path outFolder) { + mOutFolder = outFolder.toString(); + } + + /** + * Sets the value of the "basename" attribute. + * @param baseName the value. + */ + public void setBasename(String baseName) { + mBaseName = baseName; + } + + /** + * Sets the value of the "verbose" attribute. + * @param verbose the value. + */ + public void setVerbose(boolean verbose) { + mVerbose = verbose; + } + + /** + * Sets the value of the "signed" attribute. + * @param signed the value. + */ + public void setSigned(boolean signed) { + mSigned = signed; + } + + /** + * Sets the value of the "debug" attribute. + * @param debug the debug mode value. + */ + public void setDebug(boolean debug) { + mDebug = debug; + } + + /** + * Returns an object representing a nested zip element. + */ + public Object createZip() { + Value zip = new Value(); + mZipList.add(zip); + return zip; + } + + /** + * Returns an object representing a nested file element. + */ + public Object createFile() { + Value file = new Value(); + mFileList.add(file); + return file; + } + + /** + * Returns an object representing a nested sourcefolder element. + */ + public Object createSourcefolder() { + Value file = new Value(); + mSourceList.add(file); + return file; + } + + /** + * Returns an object representing a nested jarfolder element. + */ + public Object createJarfolder() { + Value file = new Value(); + mJarfolderList.add(file); + return file; + } + + /** + * Returns an object representing a nested jarfile element. + */ + public Object createJarfile() { + Value file = new Value(); + mJarfileList.add(file); + return file; + } + + /** + * Returns an object representing a nested nativefolder element. + */ + public Object createNativefolder() { + Value file = new Value(); + mNativeList.add(file); + return file; + } + + @Override + public void execute() throws BuildException { + Project antProject = getProject(); + + ApkBuilderImpl apkBuilder = new ApkBuilderImpl(); + apkBuilder.setVerbose(mVerbose); + apkBuilder.setSignedPackage(mSigned); + apkBuilder.setDebugMode(mDebug); + + try { + // setup the list of everything that needs to go in the archive. + + // go through the list of zip files to add. This will not include + // the resource package, which is handled separaly for each apk to create. + for (Value v : mZipList) { + FileInputStream input = new FileInputStream(v.mPath); + mZipArchives.add(input); + } + + // now go through the list of file to directly add the to the list. + for (Value v : mFileList) { + mArchiveFiles.add(ApkBuilderImpl.getInputFile(v.mPath)); + } + + // now go through the list of file to directly add the to the list. + for (Value v : mSourceList) { + ApkBuilderImpl.processSourceFolderForResource(new File(v.mPath), mJavaResources); + } + + // now go through the list of jar folders. + for (Value v : mJarfolderList) { + ApkBuilderImpl.processJar(new File(v.mPath), mResourcesJars); + } + + // now go through the list of jar files. + for (Value v : mJarfileList) { + ApkBuilderImpl.processJar(new File(v.mPath), mResourcesJars); + } + + // now the native lib folder. + for (Value v : mNativeList) { + String parameter = v.mPath; + File f = new File(parameter); + + ApkBuilderImpl.processNativeFolder(f, mDebug, mNativeLibraries); + } + + // create the Path item that will contain all the generated APKs + // for reuse by other targets (signing/zipaligning) + Path path = new Path(antProject); + + // The createApk method uses mBaseName for the base name of the packages (resources + // and apk files). + // The generated apk file name is + // debug: {base}[-{config}]-debug-unaligned.apk + // release: {base}[-{config}]-unsigned.apk + // Unfortunately for 1.5 projects and before the 'install' ant target expects the name + // of the default debug package to be {base}-debug.apk + // In order to support those package, we look for the 'out.debug.unaligned.package' + // property. If this exist, then we generate {base}[-{config}]-debug-unaligned.apk + // otherwise we generate {base}[-{config}]-debug.apk + // FIXME: Make apkbuilder export the package name used instead of + // having to keep apkbuilder and the rules file in sync + String debugPackageSuffix = "-debug-unaligned.apk"; + if (antProject.getProperty("out.debug.unaligned.package") == null + && antProject.getProperty("out-debug-unaligned-package") == null) { + debugPackageSuffix = "-debug.apk"; + } + + // first do a full resource package + createApk(apkBuilder, null /*configName*/, null /*resourceFilter*/, path, + debugPackageSuffix); + + // now see if we need to create file with filtered resources. + // Get the project base directory. + File baseDir = antProject.getBaseDir(); + ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(), + PropertyType.DEFAULT); + + ApkSettings apkSettings = ApkConfigurationHelper.getSettings(properties); + if (apkSettings != null) { + Map apkFilters = apkSettings.getResourceFilters(); + if (apkFilters.size() > 0) { + for (Entry entry : apkFilters.entrySet()) { + createApk(apkBuilder, entry.getKey(), entry.getValue(), path, + debugPackageSuffix); + } + } + } + + // finally sets the path in the project with a reference + antProject.addReference(REF_APK_PATH, path); + + } catch (FileNotFoundException e) { + throw new BuildException(e); + } catch (IllegalArgumentException e) { + throw new BuildException(e); + } catch (ApkCreationException e) { + throw new BuildException(e); + } + } + + /** + * Creates an application package. + * @param apkBuilder + * @param configName the name of the filter config. Can be null in which case a full resource + * package will be generated. + * @param resourceFilter the resource configuration filter to pass to aapt (if configName is + * non null) + * @param path Ant {@link Path} to which add the generated APKs as {@link PathElement} + * @param debugPackageSuffix suffix for the debug packages. + * @throws FileNotFoundException + * @throws ApkCreationException + */ + private void createApk(ApkBuilderImpl apkBuilder, String configName, String resourceFilter, + Path path, String debugPackageSuffix) + throws FileNotFoundException, ApkCreationException { + // All the files to be included in the archive have already been prep'ed up, except + // the resource package. + // figure out its name. + String filename; + if (configName != null && resourceFilter != null) { + filename = mBaseName + "-" + configName + ".ap_"; + } else { + filename = mBaseName + ".ap_"; + } + + // now we add it to the list of zip archive (it's just a zip file). + + // it's used as a zip archive input + FileInputStream resoucePackageZipFile = new FileInputStream(new File(mOutFolder, filename)); + mZipArchives.add(resoucePackageZipFile); + + // prepare the filename to generate. Same thing as the resource file. + if (configName != null && resourceFilter != null) { + filename = mBaseName + "-" + configName; + } else { + filename = mBaseName; + } + + if (mSigned) { + filename = filename + debugPackageSuffix; + } else { + filename = filename + "-unsigned.apk"; + } + + if (configName == null || resourceFilter == null) { + if (mSigned) { + System.out.println(String.format( + "Creating %s and signing it with a debug key...", filename)); + } else { + System.out.println(String.format( + "Creating %s for release...", filename)); + } + } else { + if (mSigned) { + System.out.println(String.format( + "Creating %1$s (with %2$s) and signing it with a debug key...", + filename, resourceFilter)); + } else { + System.out.println(String.format( + "Creating %1$s (with %2$s) for release...", + filename, resourceFilter)); + } + } + + // out File + File f = new File(mOutFolder, filename); + + // add it to the Path object + PathElement element = path.createPathElement(); + element.setLocation(f); + + // and generate the apk + apkBuilder.createPackage(f.getAbsoluteFile(), mZipArchives, + mArchiveFiles, mJavaResources, mResourcesJars, mNativeLibraries); + + // we are done. We need to remove the resource package from the list of zip archives + // in case we have another apk to generate. + mZipArchives.remove(resoucePackageZipFile); + } +} diff --git a/anttasks/src/com/android/ant/SetupTask.java b/anttasks/src/com/android/ant/SetupTask.java new file mode 100644 index 000000000..72f5d1140 --- /dev/null +++ b/anttasks/src/com/android/ant/SetupTask.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2009 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.ant; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISdkLog; +import com.android.sdklib.SdkManager; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.xml.AndroidXPathFactory; +import com.android.sdklib.xml.AndroidManifest; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.taskdefs.ImportTask; +import org.apache.tools.ant.types.Path; +import org.apache.tools.ant.types.Path.PathElement; +import org.xml.sax.InputSource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.HashSet; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; + +/** + * Setup/Import Ant task. This task accomplishes: + *
    + *
  • Gets the project target hash string from {@link ProjectProperties#PROPERTY_TARGET}, + * and resolves it to get the project's {@link IAndroidTarget}.
  • + *
  • Sets up properties so that aapt can find the android.jar in the resolved target.
  • + *
  • Sets up the boot classpath ref so that the javac task knows where to find + * the libraries. This includes the default android.jar from the resolved target but also optional + * libraries provided by the target (if any, when the target is an add-on).
  • + *
  • Imports the build rules located in the resolved target so that the build actually does + * something. This can be disabled with the attribute import set to false + *
+ * + * This is used in build.xml/template. + * + */ +public final class SetupTask extends ImportTask { + private final static String ANDROID_RULES = "android_rules.xml"; + // additional android rules for test project - depends on android_rules.xml + private final static String ANDROID_TEST_RULES = "android_test_rules.xml"; + // ant property with the path to the android.jar + private final static String PROPERTY_ANDROID_JAR = "android.jar"; + // LEGACY - compatibility with 1.6 and before + private final static String PROPERTY_ANDROID_JAR_LEGACY = "android-jar"; + // ant property with the path to the framework.jar + private final static String PROPERTY_ANDROID_AIDL = "android.aidl"; + // LEGACY - compatibility with 1.6 and before + private final static String PROPERTY_ANDROID_AIDL_LEGACY = "android-aidl"; + // ant property with the path to the aapt tool + private final static String PROPERTY_AAPT = "aapt"; + // ant property with the path to the aidl tool + private final static String PROPERTY_AIDL = "aidl"; + // ant property with the path to the dx tool + private final static String PROPERTY_DX = "dx"; + // ref id to the object containing all the boot classpaths. + private final static String REF_CLASSPATH = "android.target.classpath"; + + private boolean mDoImport = true; + + @Override + public void execute() throws BuildException { + Project antProject = getProject(); + + // get the SDK location + String sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK); + + // check if it's valid and exists + if (sdkLocation == null || sdkLocation.length() == 0) { + // LEGACY support: project created with 1.6 or before may be using a different + // property to declare the location of the SDK. At this point, we cannot + // yet check which target is running so we check both always. + sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK_LEGACY); + if (sdkLocation == null || sdkLocation.length() == 0) { + throw new BuildException("SDK Location is not set."); + } + } + + File sdk = new File(sdkLocation); + if (sdk.isDirectory() == false) { + throw new BuildException(String.format("SDK Location '%s' is not valid.", sdkLocation)); + } + + // get the target property value + String targetHashString = antProject.getProperty(ProjectProperties.PROPERTY_TARGET); + + boolean isTestProject = false; + + if (antProject.getProperty("tested.project.dir") != null) { + isTestProject = true; + } + + if (targetHashString == null) { + throw new BuildException("Android Target is not set."); + } + + // load up the sdk targets. + final ArrayList messages = new ArrayList(); + SdkManager manager = SdkManager.createManager(sdkLocation, new ISdkLog() { + public void error(Throwable t, String errorFormat, Object... args) { + if (errorFormat != null) { + messages.add(String.format("Error: " + errorFormat, args)); + } + if (t != null) { + messages.add("Error: " + t.getMessage()); + } + } + + public void printf(String msgFormat, Object... args) { + messages.add(String.format(msgFormat, args)); + } + + public void warning(String warningFormat, Object... args) { + messages.add(String.format("Warning: " + warningFormat, args)); + } + }); + + if (manager == null) { + // since we failed to parse the SDK, lets display the parsing output. + for (String msg : messages) { + System.out.println(msg); + } + throw new BuildException("Failed to parse SDK content."); + } + + // resolve it + IAndroidTarget androidTarget = manager.getTargetFromHashString(targetHashString); + + if (androidTarget == null) { + throw new BuildException(String.format( + "Unable to resolve target '%s'", targetHashString)); + } + + // display it + System.out.println("Project Target: " + androidTarget.getName()); + if (androidTarget.isPlatform() == false) { + System.out.println("Vendor: " + androidTarget.getVendor()); + System.out.println("Platform Version: " + androidTarget.getVersionName()); + } + System.out.println("API level: " + androidTarget.getVersion().getApiString()); + + // always check the manifest minSdkVersion. + checkManifest(antProject, androidTarget.getVersion()); + + // sets up the properties to find android.jar/framework.aidl/target tools + String androidJar = androidTarget.getPath(IAndroidTarget.ANDROID_JAR); + antProject.setProperty(PROPERTY_ANDROID_JAR, androidJar); + + String androidAidl = androidTarget.getPath(IAndroidTarget.ANDROID_AIDL); + antProject.setProperty(PROPERTY_ANDROID_AIDL, androidAidl); + + antProject.setProperty(PROPERTY_AAPT, androidTarget.getPath(IAndroidTarget.AAPT)); + antProject.setProperty(PROPERTY_AIDL, androidTarget.getPath(IAndroidTarget.AIDL)); + antProject.setProperty(PROPERTY_DX, androidTarget.getPath(IAndroidTarget.DX)); + + // sets up the boot classpath + + // create the Path object + Path bootclasspath = new Path(antProject); + + // create a PathElement for the framework jar + PathElement element = bootclasspath.createPathElement(); + element.setPath(androidJar); + + // create PathElement for each optional library. + IOptionalLibrary[] libraries = androidTarget.getOptionalLibraries(); + if (libraries != null) { + HashSet visitedJars = new HashSet(); + for (IOptionalLibrary library : libraries) { + String jarPath = library.getJarPath(); + if (visitedJars.contains(jarPath) == false) { + visitedJars.add(jarPath); + + element = bootclasspath.createPathElement(); + element.setPath(library.getJarPath()); + } + } + } + + // finally sets the path in the project with a reference + antProject.addReference(REF_CLASSPATH, bootclasspath); + + // find the file to import, and import it. + String templateFolder = androidTarget.getPath(IAndroidTarget.TEMPLATES); + + // LEGACY support. android_rules.xml in older platforms expects properties with + // older names. This sets those properties to make sure the rules will work. + if (androidTarget.getVersion().getApiLevel() <= 4) { // 1.6 and earlier + antProject.setProperty(PROPERTY_ANDROID_JAR_LEGACY, androidJar); + antProject.setProperty(PROPERTY_ANDROID_AIDL_LEGACY, androidAidl); + antProject.setProperty(ProjectProperties.PROPERTY_SDK_LEGACY, sdkLocation); + String appPackage = antProject.getProperty(ProjectProperties.PROPERTY_APP_PACKAGE); + if (appPackage != null && appPackage.length() > 0) { + antProject.setProperty(ProjectProperties.PROPERTY_APP_PACKAGE_LEGACY, appPackage); + } + } + + // Now the import section. This is only executed if the task actually has to import a file. + if (mDoImport) { + // make sure the file exists. + File templates = new File(templateFolder); + + if (templates.isDirectory() == false) { + throw new BuildException(String.format("Template directory '%s' is missing.", + templateFolder)); + } + + String importedRulesFileName = isTestProject ? ANDROID_TEST_RULES : ANDROID_RULES; + + // now check the rules file exists. + File rules = new File(templateFolder, importedRulesFileName); + + if (rules.isFile() == false) { + throw new BuildException(String.format("Build rules file '%s' is missing.", + templateFolder)); + } + + // set the file location to import + setFile(rules.getAbsolutePath()); + + // and import + super.execute(); + } + } + + /** + * Sets the value of the "import" attribute. + * @param value the value. + */ + public void setImport(boolean value) { + mDoImport = value; + } + + /** + * Checks the manifest minSdkVersion attribute. + * @param antProject the ant project + * @param androidVersion the version of the platform the project is compiling against. + */ + private void checkManifest(Project antProject, AndroidVersion androidVersion) { + try { + File manifest = new File(antProject.getBaseDir(), "AndroidManifest.xml"); + + XPath xPath = AndroidXPathFactory.newXPath(); + + // check the package name. + String value = xPath.evaluate( + "/" + AndroidManifest.NODE_MANIFEST + + "/@" + AndroidManifest.ATTRIBUTE_PACKAGE, + new InputSource(new FileInputStream(manifest))); + if (value != null) { // aapt will complain if it's missing. + // only need to check that the package has 2 segments + if (value.indexOf('.') == -1) { + throw new BuildException(String.format( + "Application package '%1$s' must have a minimum of 2 segments.", + value)); + } + } + + // check the minSdkVersion value + value = xPath.evaluate( + "/" + AndroidManifest.NODE_MANIFEST + + "/" + AndroidManifest.NODE_USES_SDK + + "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX + ":" + + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, + new InputSource(new FileInputStream(manifest))); + + if (androidVersion.isPreview()) { + // in preview mode, the content of the minSdkVersion must match exactly the + // platform codename. + String codeName = androidVersion.getCodename(); + if (codeName.equals(value) == false) { + throw new BuildException(String.format( + "For '%1$s' SDK Preview, attribute minSdkVersion in AndroidManifest.xml must be '%1$s'", + codeName)); + } + } else if (value.length() > 0) { + // for normal platform, we'll only display warnings if the value is lower or higher + // than the target api level. + // First convert to an int. + int minSdkValue = -1; + try { + minSdkValue = Integer.parseInt(value); + } catch (NumberFormatException e) { + // looks like it's not a number: error! + throw new BuildException(String.format( + "Attribute %1$s in AndroidManifest.xml must be an Integer!", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION)); + } + + int projectApiLevel = androidVersion.getApiLevel(); + if (minSdkValue < projectApiLevel) { + System.out.println(String.format( + "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is lower than the project target API level (%3$d)", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, + minSdkValue, projectApiLevel)); + } else if (minSdkValue > androidVersion.getApiLevel()) { + System.out.println(String.format( + "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is higher than the project target API level (%3$d)", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, + minSdkValue, projectApiLevel)); + } + } else { + // no minSdkVersion? display a warning + System.out.println( + "WARNING: No minSdkVersion value set. Application will install on all Android versions."); + } + + } catch (XPathExpressionException e) { + throw new BuildException(e); + } catch (FileNotFoundException e) { + throw new BuildException(e); + } + } +} diff --git a/anttasks/src/com/android/ant/XPathTask.java b/anttasks/src/com/android/ant/XPathTask.java new file mode 100644 index 000000000..b9cfb7108 --- /dev/null +++ b/anttasks/src/com/android/ant/XPathTask.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2009 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.ant; + +import com.android.sdklib.xml.AndroidXPathFactory; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.Path; +import org.xml.sax.InputSource; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; + +/** + * Android specific XPath task. + * The goal is to get the result of an XPath expression on Android XML files. The android namespace + * (http://schemas.android.com/apk/res/android) must be associated to the "android" prefix. + */ +public class XPathTask extends Task { + + private Path mManifestFile; + private String mProperty; + private String mExpression; + + public void setInput(Path manifestFile) { + mManifestFile = manifestFile; + } + + public void setOutput(String property) { + mProperty = property; + } + + public void setExpression(String expression) { + mExpression = expression; + } + + @Override + public void execute() throws BuildException { + try { + if (mManifestFile == null || mManifestFile.list().length == 0) { + throw new BuildException("input attribute is missing!"); + } + + if (mProperty == null) { + throw new BuildException("output attribute is missing!"); + } + + if (mExpression == null) { + throw new BuildException("expression attribute is missing!"); + } + + XPath xpath = AndroidXPathFactory.newXPath(); + + String file = mManifestFile.list()[0]; + String result = xpath.evaluate(mExpression, new InputSource(new FileInputStream(file))); + + getProject().setProperty(mProperty, result); + } catch (XPathExpressionException e) { + throw new BuildException(e); + } catch (FileNotFoundException e) { + throw new BuildException(e); + } + } +} diff --git a/apkbuilder/.classpath b/apkbuilder/.classpath new file mode 100644 index 000000000..f1768fc50 --- /dev/null +++ b/apkbuilder/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apkbuilder/.gitignore b/apkbuilder/.gitignore new file mode 100644 index 000000000..fe99505dc --- /dev/null +++ b/apkbuilder/.gitignore @@ -0,0 +1,2 @@ +bin + diff --git a/apkbuilder/.project b/apkbuilder/.project new file mode 100644 index 000000000..cc97afcdc --- /dev/null +++ b/apkbuilder/.project @@ -0,0 +1,17 @@ + + + ApkBuilder + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/apkbuilder/Android.mk b/apkbuilder/Android.mk new file mode 100644 index 000000000..bdfe5c82a --- /dev/null +++ b/apkbuilder/Android.mk @@ -0,0 +1,18 @@ +# +# 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. +# +APKBUILDER_LOCAL_DIR := $(call my-dir) +include $(APKBUILDER_LOCAL_DIR)/etc/Android.mk +include $(APKBUILDER_LOCAL_DIR)/src/Android.mk diff --git a/apkbuilder/etc/Android.mk b/apkbuilder/etc/Android.mk new file mode 100644 index 000000000..d74db1748 --- /dev/null +++ b/apkbuilder/etc/Android.mk @@ -0,0 +1,22 @@ +# +# 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. +# + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PREBUILT_EXECUTABLES := apkbuilder +include $(BUILD_HOST_PREBUILT) + diff --git a/apkbuilder/etc/apkbuilder b/apkbuilder/etc/apkbuilder new file mode 100755 index 000000000..3e7e82293 --- /dev/null +++ b/apkbuilder/etc/apkbuilder @@ -0,0 +1,81 @@ +#!/bin/sh +# Copyright 2005-2007, 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. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=apkbuilder.jar +frameworkdir="$progdir" +libdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + + +# Check args. +if [ debug = "$1" ]; then + # add this in for debugging + java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +else + java_debug= +fi + +# Mac OS X needs an additional arg, or you get an "illegal thread" complaint. +if [ `uname` = "Darwin" ]; then + os_opts="-XstartOnFirstThread" +else + os_opts= +fi + +if [ "$OSTYPE" = "cygwin" ] ; then + jarpath=`cygpath -w "$frameworkdir/$jarfile"` + progdir=`cygpath -w "$progdir"` +else + jarpath="$frameworkdir/$jarfile" +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +# might need more memory, e.g. -Xmx128M +exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Djava.library.path="$libdir" -jar "$jarpath" "$@" diff --git a/apkbuilder/etc/apkbuilder.bat b/apkbuilder/etc/apkbuilder.bat new file mode 100755 index 000000000..eaaf9b8c0 --- /dev/null +++ b/apkbuilder/etc/apkbuilder.bat @@ -0,0 +1,48 @@ +@echo off +rem Copyright (C) 2007 The Android Open Source Project +rem +rem Licensed under the Apache License, Version 2.0 (the "License"); +rem you may not use this file except in compliance with the License. +rem You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem don't modify the caller's environment +setlocal + +rem Set up prog to be the path of this script, including following symlinks, +rem and set up progdir to be the fully-qualified pathname of its directory. +set prog=%~f0 + +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 + +rem Check we have a valid Java.exe in the path. +set java_exe= +call lib\find_java.bat +if not defined java_exe goto :EOF + +set jarfile=apkbuilder.jar +set frameworkdir= +set libdir= + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=lib\ + set libdir=lib\ + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=..\framework\ + set libdir=..\lib\ + +:JarFileOk + +set jarpath=%frameworkdir%%jarfile% + +call %java_exe% -Djava.ext.dirs=%frameworkdir% -Djava.library.path=%libdir% -jar %jarpath% %* diff --git a/apkbuilder/etc/manifest.txt b/apkbuilder/etc/manifest.txt new file mode 100644 index 000000000..6aafb16d4 --- /dev/null +++ b/apkbuilder/etc/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.apkbuilder.ApkBuilder diff --git a/apkbuilder/src/Android.mk b/apkbuilder/src/Android.mk new file mode 100644 index 000000000..e403ca7ed --- /dev/null +++ b/apkbuilder/src/Android.mk @@ -0,0 +1,29 @@ +# +# 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. +# + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + androidprefs \ + jarutils + +LOCAL_MODULE := apkbuilder + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java b/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java new file mode 100644 index 000000000..236ebbb72 --- /dev/null +++ b/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java @@ -0,0 +1,119 @@ +/* + * 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.apkbuilder; + +import com.android.apkbuilder.internal.ApkBuilderImpl; + +import java.io.FileNotFoundException; + + +/** + * Command line APK builder with signing support. + */ +public final class ApkBuilder { + + public final static class WrongOptionException extends Exception { + private static final long serialVersionUID = 1L; + + public WrongOptionException(String message) { + super(message); + } + } + + public final static class ApkCreationException extends Exception { + private static final long serialVersionUID = 1L; + + public ApkCreationException(String message) { + super(message); + } + + public ApkCreationException(Throwable throwable) { + super(throwable); + } + + } + + /** + * Main method. This is meant to be called from the command line through an exec. + *

WARNING: this will call {@link System#exit(int)} if anything goes wrong. + * @param args command line arguments. + */ + public static void main(String[] args) { + try { + new ApkBuilderImpl().run(args); + } catch (WrongOptionException e) { + printUsageAndQuit(); + } catch (FileNotFoundException e) { + printAndExit(e.getMessage()); + } catch (ApkCreationException e) { + printAndExit(e.getMessage()); + } + } + + /** + * API entry point similar to the {@link #main(String[])} method. + *

Unlike {@link #main(String[])}, this will not call {@link System#exit(int)} and instead + * will throw exceptions. + * @param args command line arguments. + * @throws WrongOptionException if the command line arguments are incorrect. + * @throws FileNotFoundException if a required file was not found. + * @throws ApkCreationException if an error happened during the creation of the APK. + */ + public static void createApk(String[] args) throws FileNotFoundException, WrongOptionException, + ApkCreationException { + new ApkBuilderImpl().run(args); + } + + private static void printUsageAndQuit() { + // 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + System.err.println("A command line tool to package an Android application from various sources."); + System.err.println("Usage: apkbuilder [-v][-u][-storetype STORE_TYPE] [-z inputzip]"); + System.err.println(" [-f inputfile] [-rf input-folder] [-rj -input-path]"); + System.err.println(""); + System.err.println(" -v Verbose."); + System.err.println(" -d Debug Mode: Includes debug files in the APK file."); + System.err.println(" -u Creates an unsigned package."); + System.err.println(" -storetype Forces the KeyStore type. If ommited the default is used."); + System.err.println(""); + System.err.println(" -z Followed by the path to a zip archive."); + System.err.println(" Adds the content of the application package."); + System.err.println(""); + System.err.println(" -f Followed by the path to a file."); + System.err.println(" Adds the file to the application package."); + System.err.println(""); + System.err.println(" -rf Followed by the path to a source folder."); + System.err.println(" Adds the java resources found in that folder to the application"); + System.err.println(" package, while keeping their path relative to the source folder."); + System.err.println(""); + System.err.println(" -rj Followed by the path to a jar file or a folder containing"); + System.err.println(" jar files."); + System.err.println(" Adds the java resources found in the jar file(s) to the application"); + System.err.println(" package."); + System.err.println(""); + System.err.println(" -nf Followed by the root folder containing native libraries to"); + System.err.println(" include in the application package."); + + System.exit(1); + } + + private static void printAndExit(String... messages) { + for (String message : messages) { + System.err.println(message); + } + System.exit(1); + } +} diff --git a/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java b/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java new file mode 100644 index 000000000..d8e0123da --- /dev/null +++ b/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java @@ -0,0 +1,460 @@ +/* + * 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.apkbuilder.internal; + +import com.android.apkbuilder.ApkBuilder.WrongOptionException; +import com.android.apkbuilder.ApkBuilder.ApkCreationException; +import com.android.jarutils.DebugKeyProvider; +import com.android.jarutils.JavaResourceFilter; +import com.android.jarutils.SignedJarBuilder; +import com.android.jarutils.DebugKeyProvider.KeytoolException; +import com.android.prefs.AndroidLocation.AndroidLocationException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.regex.Pattern; + +/** + * Command line APK builder with signing support. + */ +public final class ApkBuilderImpl { + + private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$", + Pattern.CASE_INSENSITIVE); + private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$", + Pattern.CASE_INSENSITIVE); + + private final static String NATIVE_LIB_ROOT = "lib/"; + private final static String GDBSERVER_NAME = "gdbserver"; + + /** + * A File to be added to the APK archive. + *

This includes the {@link File} representing the file and its path in the archive. + */ + public final static class ApkFile { + String archivePath; + File file; + + ApkFile(File file, String path) { + this.file = file; + this.archivePath = path; + } + } + + private JavaResourceFilter mResourceFilter = new JavaResourceFilter(); + private boolean mVerbose = false; + private boolean mSignedPackage = true; + private boolean mDebugMode = false; + /** the optional type of the debug keystore. If null, the default */ + private String mStoreType = null; + + public void setVerbose(boolean verbose) { + mVerbose = verbose; + } + + public void setSignedPackage(boolean signedPackage) { + mSignedPackage = signedPackage; + } + + public void setDebugMode(boolean debugMode) { + mDebugMode = debugMode; + } + + public void run(String[] args) throws WrongOptionException, FileNotFoundException, + ApkCreationException { + if (args.length < 1) { + throw new WrongOptionException("No options specified"); + } + + // read the first args that should be a file path + File outFile = getOutFile(args[0]); + + ArrayList zipArchives = new ArrayList(); + ArrayList archiveFiles = new ArrayList(); + ArrayList javaResources = new ArrayList(); + ArrayList resourcesJars = new ArrayList(); + ArrayList nativeLibraries = new ArrayList(); + + int index = 1; + do { + String argument = args[index++]; + + if ("-v".equals(argument)) { + mVerbose = true; + } else if ("-d".equals(argument)) { + mDebugMode = true; + } else if ("-u".equals(argument)) { + mSignedPackage = false; + } else if ("-z".equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + throw new WrongOptionException("Missing value for -z"); + } + + try { + FileInputStream input = new FileInputStream(args[index++]); + zipArchives.add(input); + } catch (FileNotFoundException e) { + throw new ApkCreationException("-z file is not found"); + } + } else if ("-f". equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + throw new WrongOptionException("Missing value for -f"); + } + + archiveFiles.add(getInputFile(args[index++])); + } else if ("-rf". equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + throw new WrongOptionException("Missing value for -rf"); + } + + processSourceFolderForResource(new File(args[index++]), javaResources); + } else if ("-rj". equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + throw new WrongOptionException("Missing value for -rj"); + } + + processJar(new File(args[index++]), resourcesJars); + } else if ("-nf".equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + throw new WrongOptionException("Missing value for -nf"); + } + + processNativeFolder(new File(args[index++]), mDebugMode, nativeLibraries); + } else if ("-storetype".equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + throw new WrongOptionException("Missing value for -storetype"); + } + + mStoreType = args[index++]; + } else { + throw new WrongOptionException("Unknown argument: " + argument); + } + } while (index < args.length); + + createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars, + nativeLibraries); + } + + private File getOutFile(String filepath) throws ApkCreationException { + File f = new File(filepath); + + if (f.isDirectory()) { + throw new ApkCreationException(filepath + " is a directory!"); + } + + if (f.exists()) { // will be a file in this case. + if (f.canWrite() == false) { + throw new ApkCreationException("Cannot write " + filepath); + } + } else { + try { + if (f.createNewFile() == false) { + throw new ApkCreationException("Failed to create " + filepath); + } + } catch (IOException e) { + throw new ApkCreationException( + "Failed to create '" + filepath + "' : " + e.getMessage()); + } + } + + return f; + } + + /** + * Returns a {@link File} representing a given file path. The path must represent + * an actual existing file (not a directory). The path may be relative. + * @param filepath the path to a file. + * @return the File representing the path. + * @throws ApkCreationException if the path represents a directory or if the file does not + * exist, or cannot be read. + */ + public static File getInputFile(String filepath) throws ApkCreationException { + File f = new File(filepath); + + if (f.isDirectory()) { + throw new ApkCreationException(filepath + " is a directory!"); + } + + if (f.exists()) { + if (f.canRead() == false) { + throw new ApkCreationException("Cannot read " + filepath); + } + } else { + throw new ApkCreationException(filepath + " does not exists!"); + } + + return f; + } + + /** + * Processes a source folder and adds its java resources to a given list of {@link ApkFile}. + * @param folder the folder representing the source folder. + * @param javaResources the list of {@link ApkFile} to fill. + * @throws ApkCreationException + */ + public static void processSourceFolderForResource(File folder, + ArrayList javaResources) throws ApkCreationException { + if (folder.isDirectory()) { + // file is a directory, process its content. + File[] files = folder.listFiles(); + for (File file : files) { + processFileForResource(file, null, javaResources); + } + } else { + // not a directory? output error and quit. + if (folder.exists()) { + throw new ApkCreationException(folder.getAbsolutePath() + " is not a folder!"); + } else { + throw new ApkCreationException(folder.getAbsolutePath() + " does not exist!"); + } + } + } + + /** + * Process a jar file or a jar folder + * @param file the {@link File} to process + * @param resourcesJars the collection of FileInputStream to fill up with jar files. + * @throws FileNotFoundException + */ + public static void processJar(File file, Collection resourcesJars) + throws FileNotFoundException { + if (file.isDirectory()) { + String[] filenames = file.list(new FilenameFilter() { + public boolean accept(File dir, String name) { + return PATTERN_JAR_EXT.matcher(name).matches(); + } + }); + + for (String filename : filenames) { + File f = new File(file, filename); + processJarFile(f, resourcesJars); + } + } else { + processJarFile(file, resourcesJars); + } + } + + public static void processJarFile(File file, Collection resourcesJars) + throws FileNotFoundException { + FileInputStream input = new FileInputStream(file); + resourcesJars.add(input); + } + + /** + * Processes a {@link File} that could be a {@link ApkFile}, or a folder containing + * java resources. + * @param file the {@link File} to process. + * @param path the relative path of this file to the source folder. Can be null to + * identify a root file. + * @param javaResources the Collection of {@link ApkFile} object to fill. + */ + private static void processFileForResource(File file, String path, + Collection javaResources) { + if (file.isDirectory()) { + // a directory? we check it + if (JavaResourceFilter.checkFolderForPackaging(file.getName())) { + // if it's valid, we append its name to the current path. + if (path == null) { + path = file.getName(); + } else { + path = path + "/" + file.getName(); + } + + // and process its content. + File[] files = file.listFiles(); + for (File contentFile : files) { + processFileForResource(contentFile, path, javaResources); + } + } + } else { + // a file? we check it + if (JavaResourceFilter.checkFileForPackaging(file.getName())) { + // we append its name to the current path + if (path == null) { + path = file.getName(); + } else { + path = path + "/" + file.getName(); + } + + // and add it to the list. + javaResources.add(new ApkFile(file, path)); + } + } + } + + /** + * Process a {@link File} for native library inclusion. + *

The root folder must include folders that include .so files. + * @param root the native root folder. + * @param nativeLibraries the collection to add native libraries to. + * @throws ApkCreationException + */ + public static void processNativeFolder(File root, boolean debugMode, + Collection nativeLibraries) throws ApkCreationException { + if (root.isDirectory() == false) { + throw new ApkCreationException(root.getAbsolutePath() + " is not a folder!"); + } + + File[] abiList = root.listFiles(); + + if (abiList != null) { + for (File abi : abiList) { + if (abi.isDirectory()) { // ignore files + File[] libs = abi.listFiles(); + if (libs != null) { + for (File lib : libs) { + // only consider files that are .so or, if in debug mode, that + // are gdbserver executables + if (lib.isFile() && + (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() || + (debugMode && GDBSERVER_NAME.equals(lib.getName())))) { + String path = + NATIVE_LIB_ROOT + abi.getName() + "/" + lib.getName(); + + nativeLibraries.add(new ApkFile(lib, path)); + } + } + } + } + } + } + } + + /** + * Creates the application package + * @param outFile the package file to create + * @param zipArchives the list of zip archive + * @param files the list of files to include in the archive + * @param javaResources the list of java resources from the source folders. + * @param resourcesJars the list of jar files from which to take java resources + * @throws ApkCreationException + */ + public void createPackage(File outFile, Iterable zipArchives, + Iterable files, Iterable javaResources, + Iterable resourcesJars, + Iterable nativeLibraries) throws ApkCreationException { + + // get the debug key + try { + SignedJarBuilder builder; + + if (mSignedPackage) { + System.err.println(String.format("Using keystore: %s", + DebugKeyProvider.getDefaultKeyStoreOsPath())); + + + DebugKeyProvider keyProvider = new DebugKeyProvider( + null /* osKeyPath: use default */, + mStoreType, null /* IKeyGenOutput */); + PrivateKey key = keyProvider.getDebugKey(); + X509Certificate certificate = (X509Certificate)keyProvider.getCertificate(); + + if (key == null) { + throw new ApkCreationException("Unable to get debug signature key"); + } + + // compare the certificate expiration date + if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) { + // TODO, regenerate a new one. + throw new ApkCreationException("Debug Certificate expired on " + + DateFormat.getInstance().format(certificate.getNotAfter())); + } + + builder = new SignedJarBuilder( + new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key, + certificate); + } else { + builder = new SignedJarBuilder( + new FileOutputStream(outFile.getAbsolutePath(), false /* append */), + null /* key */, null /* certificate */); + } + + // add the archives + for (FileInputStream input : zipArchives) { + builder.writeZip(input, null /* filter */); + } + + // add the single files + for (File input : files) { + // always put the file at the root of the archive in this case + builder.writeFile(input, input.getName()); + if (mVerbose) { + System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(), + input.getName())); + } + } + + // add the java resource from the source folders. + for (ApkFile resource : javaResources) { + builder.writeFile(resource.file, resource.archivePath); + if (mVerbose) { + System.err.println(String.format("%1$s => %2$s", + resource.file.getAbsolutePath(), resource.archivePath)); + } + } + + // add the java resource from jar files. + for (FileInputStream input : resourcesJars) { + builder.writeZip(input, mResourceFilter); + } + + // add the native files + for (ApkFile file : nativeLibraries) { + builder.writeFile(file.file, file.archivePath); + if (mVerbose) { + System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(), + file.archivePath)); + } + } + + // close and sign the application package. + builder.close(); + } catch (KeytoolException e) { + if (e.getJavaHome() == null) { + throw new ApkCreationException(e.getMessage() + + "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" + + "You can also manually execute the following command\n:" + + e.getCommandLine()); + } else { + throw new ApkCreationException(e.getMessage() + + "\nJAVA_HOME is set to: " + e.getJavaHome() + + "\nUpdate it if necessary, or manually execute the following command:\n" + + e.getCommandLine()); + } + } catch (AndroidLocationException e) { + throw new ApkCreationException(e); + } catch (Exception e) { + throw new ApkCreationException(e); + } + } +} diff --git a/archquery/.classpath b/archquery/.classpath new file mode 100644 index 000000000..fb5011632 --- /dev/null +++ b/archquery/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/archquery/.gitignore b/archquery/.gitignore new file mode 100644 index 000000000..ba077a403 --- /dev/null +++ b/archquery/.gitignore @@ -0,0 +1 @@ +bin diff --git a/archquery/.project b/archquery/.project new file mode 100644 index 000000000..98860915d --- /dev/null +++ b/archquery/.project @@ -0,0 +1,17 @@ + + + archquery + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/archquery/Android.mk b/archquery/Android.mk new file mode 100644 index 000000000..53cad467f --- /dev/null +++ b/archquery/Android.mk @@ -0,0 +1,17 @@ +# +# Copyright (C) 2009 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. +# +ARCHQUERY_LOCAL_DIR := $(call my-dir) +include $(ARCHQUERY_LOCAL_DIR)/src/Android.mk diff --git a/archquery/etc/manifest.txt b/archquery/etc/manifest.txt new file mode 100644 index 000000000..a362e885b --- /dev/null +++ b/archquery/etc/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.archquery.Main diff --git a/archquery/src/Android.mk b/archquery/src/Android.mk new file mode 100644 index 000000000..980f0026a --- /dev/null +++ b/archquery/src/Android.mk @@ -0,0 +1,25 @@ +# +# Copyright (C) 2009 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + +LOCAL_MODULE := archquery + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/archquery/src/com/android/archquery/Main.java b/archquery/src/com/android/archquery/Main.java new file mode 100644 index 000000000..030cfe598 --- /dev/null +++ b/archquery/src/com/android/archquery/Main.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2009 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.archquery; + +/** + * Java command line tool to return the CPU architecture of the host java VM. + * + * The goal is to be able to launch SWT based applications (DDMS, Traceview, Android) on any + * type of OS. + * + * Because a 64 bit OS can run a 32 bit Virtual Machine, we need to query the VM itself to know + * whether it's 32 or 64 bit to detect which swt.jar it should use (it contains native libraries). + * Simply querying the OS is not enough. + * + * The other problem is that once a VM is launched it is impossible to change its classpath to + * point the VM to the correct version of swt.jar. + * + * The solution is this small command line tool, running in the VM, and returning the value of + * the 'os.arch' property. Based on the returned value, the script launching the SWT based + * applications will configure the Java VM with the path to the correct swt.jar + * + * Because different VMs return different values for 32 and 64 bit version of x86 CPUs, the program + * handles all the possible values and normalize the returned value. + * + * At this time, the normalized values are: + * x86: 32 bit x86 + * x86_64: 64 bit x86 + * ppc: PowerPC (WARNING: the SDK doesn't actually support this architecture). + * + * + */ +public final class Main { + public static void main(String[] args) { + + for (String arg : args) { + System.out.println(System.getProperty(arg)); + } + + if (args.length == 0) { + // Values listed from http://lopica.sourceforge.net/os.html + String arch = System.getProperty("os.arch"); + + if (arch.equalsIgnoreCase("x86_64") || arch.equalsIgnoreCase("amd64")) { + System.out.print("x86_64"); + + } else if (arch.equalsIgnoreCase("x86") + || arch.equalsIgnoreCase("i386") + || arch.equalsIgnoreCase("i686")) { + System.out.print("x86"); + + } else if (arch.equalsIgnoreCase("ppc") || arch.equalsIgnoreCase("PowerPC")) { + System.out.print("ppc"); + } else { + System.out.print(arch); + } + } + } +} diff --git a/changes.txt b/changes.txt new file mode 100644 index 000000000..031ff8d7d --- /dev/null +++ b/changes.txt @@ -0,0 +1,14 @@ +Change log for Android SDK Tools. + +Revision 5 (02/2010): +- AVD/SDK Manager: + - Fixed SSL download for the standalone version of the SDK Updater. + - Fixed issue with 64bit JVM on windows. + - Add support for samples components. + - improved support for dependency between components. + - AVDs now sorted by API level. + - Prevent deletion of running AVDs. + - Settings are now automatically saved, no need to click Apply. +- Emulator now requires sd card to be 9MB and above. +- Fixed layoutopt.bat to correctly execute on Windows. + diff --git a/ddms/.gitignore b/ddms/.gitignore new file mode 100644 index 000000000..6d833a0ee --- /dev/null +++ b/ddms/.gitignore @@ -0,0 +1,4 @@ +app/bin +libs/ddmlib/bin +libs/ddmuilib/bin + diff --git a/ddms/Android.mk b/ddms/Android.mk new file mode 100644 index 000000000..82c248e43 --- /dev/null +++ b/ddms/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2007 The Android Open Source Project +# +DDMS_LOCAL_DIR := $(call my-dir) +include $(DDMS_LOCAL_DIR)/libs/Android.mk +include $(DDMS_LOCAL_DIR)/app/Android.mk diff --git a/ddms/MODULE_LICENSE_APACHE2 b/ddms/MODULE_LICENSE_APACHE2 new file mode 100644 index 000000000..e69de29bb diff --git a/ddms/app/.classpath b/ddms/app/.classpath new file mode 100644 index 000000000..1040688c3 --- /dev/null +++ b/ddms/app/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ddms/app/.project b/ddms/app/.project new file mode 100644 index 000000000..ffb19d7a9 --- /dev/null +++ b/ddms/app/.project @@ -0,0 +1,17 @@ + + + ddms + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/ddms/app/Android.mk b/ddms/app/Android.mk new file mode 100644 index 000000000..3857706a1 --- /dev/null +++ b/ddms/app/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2007 The Android Open Source Project +# +DDMSAPP_LOCAL_DIR := $(call my-dir) +include $(DDMSAPP_LOCAL_DIR)/etc/Android.mk +include $(DDMSAPP_LOCAL_DIR)/src/Android.mk diff --git a/ddms/app/README b/ddms/app/README new file mode 100644 index 000000000..cc55ddd87 --- /dev/null +++ b/ddms/app/README @@ -0,0 +1,11 @@ +Using the Eclipse projects for ddms. + +ddms requires SWT to compile. + +SWT is available in the depot under //device/prebuild//swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar +available at //device/prebuild//swt. \ No newline at end of file diff --git a/ddms/app/etc/Android.mk b/ddms/app/etc/Android.mk new file mode 100644 index 000000000..9d6997170 --- /dev/null +++ b/ddms/app/etc/Android.mk @@ -0,0 +1,8 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PREBUILT_EXECUTABLES := ddms +include $(BUILD_HOST_PREBUILT) + diff --git a/ddms/app/etc/ddms b/ddms/app/etc/ddms new file mode 100755 index 000000000..1367aeb4d --- /dev/null +++ b/ddms/app/etc/ddms @@ -0,0 +1,108 @@ +#!/bin/bash +# Copyright 2005-2007, 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. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=ddms.jar +frameworkdir="$progdir" +libdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + + +# Check args. +if [ debug = "$1" ]; then + # add this in for debugging + java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +else + java_debug= +fi + +javaCmd="java" + +# Mac OS X needs an additional arg, or you get an "illegal thread" complaint. +if [ `uname` = "Darwin" ]; then + os_opts="-XstartOnFirstThread" +else + os_opts= +fi + +if [ `uname` = "Linux" ]; then + export GDK_NATIVE_WINDOWS=true +fi + +jarpath="$frameworkdir/$jarfile" + +# Figure out the path to the swt.jar for the current architecture. +# if ANDROID_SWT is defined, then just use this. +# else, if running in the Android source tree, then look for the correct swt folder in prebuilt +# else, look for the correct swt folder in the SDK under tools/lib/ +swtpath="" +if [ -n "$ANDROID_SWT" ]; then + swtpath="$ANDROID_SWT" +else + vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar` + if [ -n "$ANDROID_BUILD_TOP" ]; then + osname=`uname -s | tr A-Z a-z` + swtpath="${ANDROID_BUILD_TOP}/prebuilt/${osname}-${vmarch}/swt" + else + swtpath="${frameworkdir}/${vmarch}" + fi +fi + +# Combine the swtpath and the framework dir path. +if [ -d "$swtpath" ]; then + frameworkdir="${swtpath}:${frameworkdir}" +else + echo "SWT folder '${swtpath}' does not exist." + echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." + exit 1 +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +# might need more memory, e.g. -Xmx128M +exec "$javaCmd" -Xmx256M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Dcom.android.ddms.bindir="$progdir" -jar "$jarpath" "$@" diff --git a/ddms/app/etc/ddms.bat b/ddms/app/etc/ddms.bat new file mode 100755 index 000000000..271ac4ca1 --- /dev/null +++ b/ddms/app/etc/ddms.bat @@ -0,0 +1,69 @@ +@echo off +rem Copyright (C) 2007 The Android Open Source Project +rem +rem Licensed under the Apache License, Version 2.0 (the "License"); +rem you may not use this file except in compliance with the License. +rem You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem don't modify the caller's environment +setlocal + +rem Set up prog to be the path of this script, including following symlinks, +rem and set up progdir to be the fully-qualified pathname of its directory. +set prog=%~f0 + +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 + +rem Check we have a valid Java.exe in the path. +set java_exe= +call lib\find_java.bat +if not defined java_exe goto :EOF + +set jarfile=ddms.jar +set frameworkdir= + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=lib\ + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=..\framework\ + +:JarFileOk + +if debug NEQ "%1" goto NoDebug + set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +:NoDebug + +set jarpath=%frameworkdir%%jarfile% + +if not defined ANDROID_SWT goto QueryArch + set swt_path=%ANDROID_SWT% + goto SwtDone + +:QueryArch + + for /f %%a in ('%java_exe% -jar %frameworkdir%archquery.jar') do set swt_path=%frameworkdir%%%a + +:SwtDone + +if exist %swt_path% goto SetPath + echo SWT folder '%swt_path%' does not exist. + echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. + exit /B + +:SetPath +set javaextdirs=%swt_path%;%frameworkdir% + +call %java_exe% %java_debug% -Djava.ext.dirs=%javaextdirs% -Dcom.android.ddms.bindir= -jar %jarpath% %* + diff --git a/ddms/app/etc/manifest.txt b/ddms/app/etc/manifest.txt new file mode 100644 index 000000000..84c8acdc1 --- /dev/null +++ b/ddms/app/etc/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.ddms.Main diff --git a/ddms/app/src/Android.mk b/ddms/app/src/Android.mk new file mode 100644 index 000000000..c62b67808 --- /dev/null +++ b/ddms/app/src/Android.mk @@ -0,0 +1,22 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAVA_RESOURCE_DIRS := resources + +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + androidprefs \ + sdkstats \ + ddmlib \ + ddmuilib \ + swt \ + org.eclipse.jface_3.4.2.M20090107-0800 \ + org.eclipse.equinox.common_3.4.0.v20080421-2006 \ + org.eclipse.core.commands_3.4.0.I20080509-2000 +LOCAL_MODULE := ddms + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/ddms/app/src/com/android/ddms/AboutDialog.java b/ddms/app/src/com/android/ddms/AboutDialog.java new file mode 100644 index 000000000..e946aee7e --- /dev/null +++ b/ddms/app/src/com/android/ddms/AboutDialog.java @@ -0,0 +1,157 @@ +/* //device/tools/ddms/src/com/android/ddms/AboutDialog.java +** +** Copyright 2007, 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.ddms; + +import com.android.ddmlib.Log; +import com.android.ddmuilib.ImageHelper; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.io.InputStream; + +/** + * Our "about" box. + */ +public class AboutDialog extends Dialog { + + private Image logoImage; + + /** + * Create with default style. + */ + public AboutDialog(Shell parent) { + this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + } + + /** + * Create with app-defined style. + */ + public AboutDialog(Shell parent, int style) { + super(parent, style); + } + + /** + * Prepare and display the dialog. + */ + public void open() { + Shell parent = getParent(); + Shell shell = new Shell(parent, getStyle()); + shell.setText("About..."); + + logoImage = loadImage(shell, "ddms-logo.png"); // $NON-NLS-1$ + createContents(shell); + shell.pack(); + + shell.open(); + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + logoImage.dispose(); + } + + /* + * Load an image file from a resource. + * + * This depends on Display, so I'm not sure what the rules are for + * loading once and caching in a static class field. + */ + private Image loadImage(Shell shell, String fileName) { + InputStream imageStream; + String pathName = "/images/" + fileName; // $NON-NLS-1$ + + imageStream = this.getClass().getResourceAsStream(pathName); + if (imageStream == null) { + //throw new NullPointerException("couldn't find " + pathName); + Log.w("ddms", "Couldn't load " + pathName); + Display display = shell.getDisplay(); + return ImageHelper.createPlaceHolderArt(display, 100, 50, + display.getSystemColor(SWT.COLOR_BLUE)); + } + + Image img = new Image(shell.getDisplay(), imageStream); + if (img == null) + throw new NullPointerException("couldn't load " + pathName); + return img; + } + + /* + * Create the about box contents. + */ + private void createContents(final Shell shell) { + GridLayout layout; + GridData data; + Label label; + + shell.setLayout(new GridLayout(2, false)); + + // Fancy logo + Label logo = new Label(shell, SWT.BORDER); + logo.setImage(logoImage); + + // Text Area + Composite textArea = new Composite(shell, SWT.NONE); + layout = new GridLayout(1, true); + textArea.setLayout(layout); + + // Text lines + label = new Label(textArea, SWT.NONE); + if (Main.sRevision != null && Main.sRevision.length() > 0) { + label.setText("Dalvik Debug Monitor Revision " + Main.sRevision); + } else { + label.setText("Dalvik Debug Monitor"); + } + label = new Label(textArea, SWT.NONE); + label.setText("Copyright 2007, The Android Open Source Project"); + label = new Label(textArea, SWT.NONE); + label.setText("All Rights Reserved."); + + // blank spot in grid + label = new Label(shell, SWT.NONE); + + // "OK" button + Button ok = new Button(shell, SWT.PUSH); + ok.setText("OK"); + data = new GridData(GridData.HORIZONTAL_ALIGN_END); + data.widthHint = 80; + ok.setLayoutData(data); + ok.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + shell.close(); + } + }); + + shell.pack(); + + shell.setDefaultButton(ok); + } +} diff --git a/ddms/app/src/com/android/ddms/DebugPortProvider.java b/ddms/app/src/com/android/ddms/DebugPortProvider.java new file mode 100644 index 000000000..76a86b74c --- /dev/null +++ b/ddms/app/src/com/android/ddms/DebugPortProvider.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2007 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.ddms; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; + +import org.eclipse.jface.preference.IPreferenceStore; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * DDMS implementation of the IDebugPortProvider interface. + * This class handles saving/loading the list of static debug port from + * the preference store and provides the port number to the Device Monitor. + */ +public class DebugPortProvider implements IDebugPortProvider { + + private static DebugPortProvider sThis = new DebugPortProvider(); + + /** Preference name for the static port list. */ + public static final String PREFS_STATIC_PORT_LIST = "android.staticPortList"; //$NON-NLS-1$ + + /** + * Mapping device serial numbers to maps. The embedded maps are mapping application names to + * debugger ports. + */ + private Map> mMap; + + public static DebugPortProvider getInstance() { + return sThis; + } + + private DebugPortProvider() { + computePortList(); + } + + /** + * Returns a static debug port for the specified application running on the + * specified {@link IDevice}. + * @param device The device the application is running on. + * @param appName The application name, as defined in the + * AndroidManifest.xml package attribute. + * @return The static debug port or {@link #NO_STATIC_PORT} if there is none setup. + * + * @see IDebugPortProvider#getPort(IDevice, String) + */ + public int getPort(IDevice device, String appName) { + if (mMap != null) { + Map deviceMap = mMap.get(device.getSerialNumber()); + if (deviceMap != null) { + Integer i = deviceMap.get(appName); + if (i != null) { + return i.intValue(); + } + } + } + return IDebugPortProvider.NO_STATIC_PORT; + } + + /** + * Returns the map of Static debugger ports. The map links device serial numbers to + * a map linking application name to debugger ports. + */ + public Map> getPortList() { + return mMap; + } + + /** + * Create the map member from the values contained in the Preference Store. + */ + private void computePortList() { + mMap = new HashMap>(); + + // get the prefs store + IPreferenceStore store = PrefsDialog.getStore(); + String value = store.getString(PREFS_STATIC_PORT_LIST); + + if (value != null && value.length() > 0) { + // format is + // port1|port2|port3|... + // where port# is + // appPackageName:appPortNumber:device-serial-number + String[] portSegments = value.split("\\|"); //$NON-NLS-1$ + for (String seg : portSegments) { + String[] entry = seg.split(":"); //$NON-NLS-1$ + + // backward compatibility support. if we have only 2 entry, we default + // to the first emulator. + String deviceName = null; + if (entry.length == 3) { + deviceName = entry[2]; + } else { + deviceName = IDevice.FIRST_EMULATOR_SN; + } + + // get the device map + Map deviceMap = mMap.get(deviceName); + if (deviceMap == null) { + deviceMap = new HashMap(); + mMap.put(deviceName, deviceMap); + } + + deviceMap.put(entry[0], Integer.valueOf(entry[1])); + } + } + } + + /** + * Sets new [device, app, port] values. + * The values are also sync'ed in the preference store. + * @param map The map containing the new values. + */ + public void setPortList(Map> map) { + // update the member map. + mMap.clear(); + mMap.putAll(map); + + // create the value to store in the preference store. + // see format definition in getPortList + StringBuilder sb = new StringBuilder(); + + Set deviceKeys = map.keySet(); + for (String deviceKey : deviceKeys) { + Map deviceMap = map.get(deviceKey); + if (deviceMap != null) { + Set appKeys = deviceMap.keySet(); + + for (String appKey : appKeys) { + Integer port = deviceMap.get(appKey); + if (port != null) { + sb.append(appKey).append(':').append(port.intValue()).append(':'). + append(deviceKey).append('|'); + } + } + } + } + + String value = sb.toString(); + + // get the prefs store. + IPreferenceStore store = PrefsDialog.getStore(); + + // and give it the new value. + store.setValue(PREFS_STATIC_PORT_LIST, value); + } +} diff --git a/ddms/app/src/com/android/ddms/DeviceCommandDialog.java b/ddms/app/src/com/android/ddms/DeviceCommandDialog.java new file mode 100644 index 000000000..614573350 --- /dev/null +++ b/ddms/app/src/com/android/ddms/DeviceCommandDialog.java @@ -0,0 +1,423 @@ +/* //device/tools/ddms/src/com/android/ddms/DeviceCommandDialog.java +** +** Copyright 2007, 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.ddms; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + + +/** + * Execute a command on an ADB-attached device and save the output. + * + * There are several ways to do this. One is to run a single command + * and show the output. Another is to have several possible commands and + * let the user click a button next to the one (or ones) they want. This + * currently uses the simple 1:1 form. + */ +public class DeviceCommandDialog extends Dialog { + + public static final int DEVICE_STATE = 0; + public static final int APP_STATE = 1; + public static final int RADIO_STATE = 2; + public static final int LOGCAT = 3; + + private String mCommand; + private String mFileName; + + private Label mStatusLabel; + private Button mCancelDone; + private Button mSave; + private Text mText; + private Font mFont = null; + private boolean mCancel; + private boolean mFinished; + + + /** + * Create with default style. + */ + public DeviceCommandDialog(String command, String fileName, Shell parent) { + // don't want a close button, but it seems hard to get rid of on GTK + // keep it on all platforms for consistency + this(command, fileName, parent, + SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL | SWT.RESIZE); + } + + /** + * Create with app-defined style. + */ + public DeviceCommandDialog(String command, String fileName, Shell parent, + int style) + { + super(parent, style); + mCommand = command; + mFileName = fileName; + } + + /** + * Prepare and display the dialog. + * @param currentDevice + */ + public void open(IDevice currentDevice) { + Shell parent = getParent(); + Shell shell = new Shell(parent, getStyle()); + shell.setText("Remote Command"); + + mFinished = false; + mFont = findFont(shell.getDisplay()); + createContents(shell); + + // Getting weird layout behavior under Linux when Text is added -- + // looks like text widget has min width of 400 when FILL_HORIZONTAL + // is used, and layout gets tweaked to force this. (Might be even + // more with the scroll bars in place -- it wigged out when the + // file save dialog was invoked.) + shell.setMinimumSize(500, 200); + shell.setSize(800, 600); + shell.open(); + + executeCommand(shell, currentDevice); + + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + if (mFont != null) + mFont.dispose(); + } + + /* + * Create a text widget to show the output and some buttons to + * manage things. + */ + private void createContents(final Shell shell) { + GridData data; + + shell.setLayout(new GridLayout(2, true)); + + shell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + if (!mFinished) { + Log.d("ddms", "NOT closing - cancelling command"); + event.doit = false; + mCancel = true; + } + } + }); + + mStatusLabel = new Label(shell, SWT.NONE); + mStatusLabel.setText("Executing '" + shortCommandString() + "'"); + data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); + data.horizontalSpan = 2; + mStatusLabel.setLayoutData(data); + + mText = new Text(shell, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + mText.setEditable(false); + mText.setFont(mFont); + data = new GridData(GridData.FILL_BOTH); + data.horizontalSpan = 2; + mText.setLayoutData(data); + + // "save" button + mSave = new Button(shell, SWT.PUSH); + mSave.setText("Save"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + mSave.setLayoutData(data); + mSave.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + saveText(shell); + } + }); + mSave.setEnabled(false); + + // "cancel/done" button + mCancelDone = new Button(shell, SWT.PUSH); + mCancelDone.setText("Cancel"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + mCancelDone.setLayoutData(data); + mCancelDone.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (!mFinished) + mCancel = true; + else + shell.close(); + } + }); + } + + /* + * Figure out what font to use. + * + * Returns "null" if we can't figure it out, which SWT understands to + * mean "use default system font". + */ + private Font findFont(Display display) { + String fontStr = PrefsDialog.getStore().getString("textOutputFont"); + if (fontStr != null) { + FontData fdat = new FontData(fontStr); + if (fdat != null) + return new Font(display, fdat); + } + return null; + } + + + /* + * Callback class for command execution. + */ + class Gatherer extends Thread implements IShellOutputReceiver { + public static final int RESULT_UNKNOWN = 0; + public static final int RESULT_SUCCESS = 1; + public static final int RESULT_FAILURE = 2; + public static final int RESULT_CANCELLED = 3; + + private Shell mShell; + private String mCommand; + private Text mText; + private int mResult; + private IDevice mDevice; + + /** + * Constructor; pass in the text widget that will receive the output. + * @param device + */ + public Gatherer(Shell shell, IDevice device, String command, Text text) { + mShell = shell; + mDevice = device; + mCommand = command; + mText = text; + mResult = RESULT_UNKNOWN; + + // this is in outer class + mCancel = false; + } + + /** + * Thread entry point. + */ + @Override + public void run() { + + if (mDevice == null) { + Log.w("ddms", "Cannot execute command: no device selected."); + mResult = RESULT_FAILURE; + } else { + try { + mDevice.executeShellCommand(mCommand, this); + if (mCancel) + mResult = RESULT_CANCELLED; + else + mResult = RESULT_SUCCESS; + } + catch (IOException ioe) { + Log.w("ddms", "Remote exec failed: " + ioe.getMessage()); + mResult = RESULT_FAILURE; + } + } + + mShell.getDisplay().asyncExec(new Runnable() { + public void run() { + updateForResult(mResult); + } + }); + } + + /** + * Called by executeRemoteCommand(). + */ + public void addOutput(byte[] data, int offset, int length) { + + Log.v("ddms", "received " + length + " bytes"); + try { + final String text; + text = new String(data, offset, length, "ISO-8859-1"); + + // add to text widget; must do in UI thread + mText.getDisplay().asyncExec(new Runnable() { + public void run() { + mText.append(text); + } + }); + } + catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + } + } + + public void flush() { + // nothing to flush. + } + + /** + * Called by executeRemoteCommand(). + */ + public boolean isCancelled() { + return mCancel; + } + }; + + /* + * Execute a remote command, add the output to the text widget, and + * update controls. + * + * We have to run the command in a thread so that the UI continues + * to work. + */ + private void executeCommand(Shell shell, IDevice device) { + Gatherer gath = new Gatherer(shell, device, commandString(), mText); + gath.start(); + } + + /* + * Update the controls after the remote operation completes. This + * must be called from the UI thread. + */ + private void updateForResult(int result) { + if (result == Gatherer.RESULT_SUCCESS) { + mStatusLabel.setText("Successfully executed '" + + shortCommandString() + "'"); + mSave.setEnabled(true); + } else if (result == Gatherer.RESULT_CANCELLED) { + mStatusLabel.setText("Execution cancelled; partial results below"); + mSave.setEnabled(true); // save partial + } else if (result == Gatherer.RESULT_FAILURE) { + mStatusLabel.setText("Failed"); + } + mStatusLabel.pack(); + mCancelDone.setText("Done"); + mFinished = true; + } + + /* + * Allow the user to save the contents of the text dialog. + */ + private void saveText(Shell shell) { + FileDialog dlg = new FileDialog(shell, SWT.SAVE); + String fileName; + + dlg.setText("Save output..."); + dlg.setFileName(defaultFileName()); + dlg.setFilterPath(PrefsDialog.getStore().getString("lastTextSaveDir")); + dlg.setFilterNames(new String[] { + "Text Files (*.txt)" + }); + dlg.setFilterExtensions(new String[] { + "*.txt" + }); + + fileName = dlg.open(); + if (fileName != null) { + PrefsDialog.getStore().setValue("lastTextSaveDir", + dlg.getFilterPath()); + + Log.d("ddms", "Saving output to " + fileName); + + /* + * Convert to 8-bit characters. + */ + String text = mText.getText(); + byte[] ascii; + try { + ascii = text.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); + ascii = new byte[0]; + } + + /* + * Output data, converting CRLF to LF. + */ + try { + int length = ascii.length; + + FileOutputStream outFile = new FileOutputStream(fileName); + BufferedOutputStream out = new BufferedOutputStream(outFile); + for (int i = 0; i < length; i++) { + if (i < length-1 && + ascii[i] == 0x0d && ascii[i+1] == 0x0a) + { + continue; + } + out.write(ascii[i]); + } + out.close(); // flush buffer, close file + } + catch (IOException ioe) { + Log.w("ddms", "Unable to save " + fileName + ": " + ioe); + } + } + } + + + /* + * Return the shell command we're going to use. + */ + private String commandString() { + return mCommand; + + } + + /* + * Return a default filename for the "save" command. + */ + private String defaultFileName() { + return mFileName; + } + + /* + * Like commandString(), but length-limited. + */ + private String shortCommandString() { + String str = commandString(); + if (str.length() > 50) + return str.substring(0, 50) + "..."; + else + return str; + } +} + diff --git a/ddms/app/src/com/android/ddms/DropdownSelectionListener.java b/ddms/app/src/com/android/ddms/DropdownSelectionListener.java new file mode 100644 index 000000000..04d921c49 --- /dev/null +++ b/ddms/app/src/com/android/ddms/DropdownSelectionListener.java @@ -0,0 +1,80 @@ +/* //device/tools/ddms/src/com/android/ddms/DropdownSelectionListener.java +** +** Copyright 2007, 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.ddms; + +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.ToolItem; + +/** + * Helper class for drop-down menus in toolbars. + */ +public class DropdownSelectionListener extends SelectionAdapter { + private Menu mMenu; + private ToolItem mDropdown; + + /** + * Basic constructor. Creates an empty Menu to hold items. + */ + public DropdownSelectionListener(ToolItem item) { + mDropdown = item; + mMenu = new Menu(item.getParent().getShell(), SWT.POP_UP); + } + + /** + * Add an item to the dropdown menu. + */ + public void add(String label) { + MenuItem item = new MenuItem(mMenu, SWT.NONE); + item.setText(label); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // update the dropdown's text to match the selection + MenuItem sel = (MenuItem) e.widget; + mDropdown.setText(sel.getText()); + } + }); + } + + /** + * Invoked when dropdown or neighboring arrow is clicked. + */ + @Override + public void widgetSelected(SelectionEvent e) { + if (e.detail == SWT.ARROW) { + // arrow clicked, show menu + ToolItem item = (ToolItem) e.widget; + Rectangle rect = item.getBounds(); + Point pt = item.getParent().toDisplay(new Point(rect.x, rect.y)); + mMenu.setLocation(pt.x, pt.y + rect.height); + mMenu.setVisible(true); + } else { + // button clicked + Log.d("ddms", mDropdown.getText() + " Pressed"); + } + } +} + diff --git a/ddms/app/src/com/android/ddms/Main.java b/ddms/app/src/com/android/ddms/Main.java new file mode 100644 index 000000000..050519f51 --- /dev/null +++ b/ddms/app/src/com/android/ddms/Main.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2007 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.ddms; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.DebugPortManager; +import com.android.ddmlib.Log; +import com.android.sdkstats.SdkStatsService; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.util.Properties; + + +/** + * Start the UI and network. + */ +public class Main { + + public static String sRevision; + + public Main() { + } + + /* + * If a thread bails with an uncaught exception, bring the whole + * thing down. + */ + private static class UncaughtHandler implements Thread.UncaughtExceptionHandler { + public void uncaughtException(Thread t, Throwable e) { + Log.e("ddms", "shutting down due to uncaught exception"); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + Log.e("ddms", sw.toString()); + + System.exit(1); + } + } + + /** + * Parse args, start threads. + */ + public static void main(String[] args) { + // In order to have the AWT/SWT bridge work on Leopard, we do this little hack. + String os = System.getProperty("os.name"); //$NON-NLS-1$ + if (os.startsWith("Mac OS")) { //$NON-NLS-1$ + RuntimeMXBean rt = ManagementFactory.getRuntimeMXBean(); + System.setProperty( + "JAVA_STARTED_ON_FIRST_THREAD_" + (rt.getName().split("@"))[0], //$NON-NLS-1$ + "1"); //$NON-NLS-1$ + } + + Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler()); + + // load prefs and init the default values + PrefsDialog.init(); + + Log.d("ddms", "Initializing"); + + // the "ping" argument means to check in with the server and exit + // the application name and version number must also be supplied + if (args.length >= 3 && args[0].equals("ping")) { + SdkStatsService.ping(args[1], args[2], null); + return; + } else if (args.length > 0) { + Log.e("ddms", "Unknown argument: " + args[0]); + System.exit(1); + } + + // get the ddms parent folder location + String ddmsParentLocation = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$ + + // we're past the point where ddms can be called just to send a ping, so we can + // ping for ddms itself. + ping(ddmsParentLocation); + + DebugPortManager.setProvider(DebugPortProvider.getInstance()); + + // create the three main threads + UIThread ui = UIThread.getInstance(); + + try { + ui.runUI(ddmsParentLocation); + } finally { + PrefsDialog.save(); + + AndroidDebugBridge.terminate(); + } + + Log.d("ddms", "Bye"); + + // this is kinda bad, but on MacOS the shutdown doesn't seem to finish because of + // a thread called AWT-Shutdown. This will help while I track this down. + System.exit(0); + } + + public static void ping(String ddmsParentLocation) { + Properties p = new Properties(); + try{ + File sourceProp; + if (ddmsParentLocation != null && ddmsParentLocation.length() > 0) { + sourceProp = new File(ddmsParentLocation, "source.properties"); //$NON-NLS-1$ + } else { + sourceProp = new File("source.properties"); //$NON-NLS-1$ + } + p.load(new FileInputStream(sourceProp)); + sRevision = p.getProperty("Pkg.Revision"); //$NON-NLS-1$ + if (sRevision != null && sRevision.length() > 0) { + SdkStatsService.ping("ddms", sRevision, null); //$NON-NLS-1$ + } + } catch (FileNotFoundException e) { + // couldn't find the file? don't ping. + } catch (IOException e) { + // couldn't find the file? don't ping. + } + } +} diff --git a/ddms/app/src/com/android/ddms/PrefsDialog.java b/ddms/app/src/com/android/ddms/PrefsDialog.java new file mode 100644 index 000000000..fbdbfe695 --- /dev/null +++ b/ddms/app/src/com/android/ddms/PrefsDialog.java @@ -0,0 +1,529 @@ +/* //device/tools/ddms/src/com/android/ddms/PrefsDialog.java +** +** Copyright 2007, 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.ddms; + +import com.android.ddmlib.DdmConstants; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.PortFieldEditor; +import com.android.sdkstats.SdkStatsService; + +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.DirectoryFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.FontFieldEditor; +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.preference.PreferenceManager; +import org.eclipse.jface.preference.PreferenceNode; +import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.jface.preference.PreferenceStore; +import org.eclipse.jface.preference.RadioGroupFieldEditor; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.IOException; + +/** + * Preferences dialog. + */ +public final class PrefsDialog { + + // Preference store. + private static PreferenceStore mPrefStore; + + // public const values for storage + public final static String SHELL_X = "shellX"; //$NON-NLS-1$ + public final static String SHELL_Y = "shellY"; //$NON-NLS-1$ + public final static String SHELL_WIDTH = "shellWidth"; //$NON-NLS-1$ + public final static String SHELL_HEIGHT = "shellHeight"; //$NON-NLS-1$ + public final static String EXPLORER_SHELL_X = "explorerShellX"; //$NON-NLS-1$ + public final static String EXPLORER_SHELL_Y = "explorerShellY"; //$NON-NLS-1$ + public final static String EXPLORER_SHELL_WIDTH = "explorerShellWidth"; //$NON-NLS-1$ + public final static String EXPLORER_SHELL_HEIGHT = "explorerShellHeight"; //$NON-NLS-1$ + public final static String SHOW_NATIVE_HEAP = "native"; //$NON-NLS-1$ + + public final static String LOGCAT_COLUMN_MODE = "ddmsLogColumnMode"; //$NON-NLS-1$ + public final static String LOGCAT_FONT = "ddmsLogFont"; //$NON-NLS-1$ + + public final static String LOGCAT_COLUMN_MODE_AUTO = "auto"; //$NON-NLS-1$ + public final static String LOGCAT_COLUMN_MODE_MANUAL = "manual"; //$NON-NLS-1$ + + private final static String PREFS_DEBUG_PORT_BASE = "adbDebugBasePort"; //$NON-NLS-1$ + private final static String PREFS_SELECTED_DEBUG_PORT = "debugSelectedPort"; //$NON-NLS-1$ + private final static String PREFS_DEFAULT_THREAD_UPDATE = "defaultThreadUpdateEnabled"; //$NON-NLS-1$ + private final static String PREFS_DEFAULT_HEAP_UPDATE = "defaultHeapUpdateEnabled"; //$NON-NLS-1$ + private final static String PREFS_THREAD_REFRESH_INTERVAL = "threadStatusInterval"; //$NON-NLS-1$ + private final static String PREFS_LOG_LEVEL = "ddmsLogLevel"; //$NON-NLS-1$ + private final static String PREFS_TIMEOUT = "timeOut"; //$NON-NLS-1$ + + + /** + * Private constructor -- do not instantiate. + */ + private PrefsDialog() {} + + /** + * Return the PreferenceStore that holds our values. + */ + public static PreferenceStore getStore() { + return mPrefStore; + } + + /** + * Save the prefs to the config file. + */ + public static void save() { + try { + mPrefStore.save(); + } + catch (IOException ioe) { + Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage()); + } + } + + /** + * Do some one-time prep. + * + * The original plan was to let the individual classes define their + * own defaults, which we would get and then override with the config + * file. However, PreferencesStore.load() doesn't trigger the "changed" + * events, which means we have to pull the loaded config values out by + * hand. + * + * So, we set the defaults, load the values from the config file, and + * then run through and manually export the values. Then we duplicate + * the second part later on for the "changed" events. + */ + public static void init() { + assert mPrefStore == null; + + mPrefStore = SdkStatsService.getPreferenceStore(); + + if (mPrefStore == null) { + // we have a serious issue here... + Log.e("ddms", + "failed to access both the user HOME directory and the system wide temp folder. Quitting."); + System.exit(1); + } + + // configure default values + setDefaults(System.getProperty("user.home")); //$NON-NLS-1$ + + // listen for changes + mPrefStore.addPropertyChangeListener(new ChangeListener()); + + // Now we initialize the value of the preference, from the values in the store. + + // First the ddm lib. + DdmPreferences.setDebugPortBase(mPrefStore.getInt(PREFS_DEBUG_PORT_BASE)); + DdmPreferences.setSelectedDebugPort(mPrefStore.getInt(PREFS_SELECTED_DEBUG_PORT)); + DdmPreferences.setLogLevel(mPrefStore.getString(PREFS_LOG_LEVEL)); + DdmPreferences.setInitialThreadUpdate(mPrefStore.getBoolean(PREFS_DEFAULT_THREAD_UPDATE)); + DdmPreferences.setInitialHeapUpdate(mPrefStore.getBoolean(PREFS_DEFAULT_HEAP_UPDATE)); + DdmPreferences.setTimeOut(mPrefStore.getInt(PREFS_TIMEOUT)); + + // some static values + String out = System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$ + DdmUiPreferences.setSymbolsLocation(out + File.separator + "symbols"); //$NON-NLS-1$ + DdmUiPreferences.setAddr2LineLocation("arm-eabi-addr2line"); //$NON-NLS-1$ + + String traceview = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$ + if (traceview != null && traceview.length() != 0) { + traceview += File.separator + DdmConstants.FN_TRACEVIEW; + } else { + traceview = DdmConstants.FN_TRACEVIEW; + } + DdmUiPreferences.setTraceviewLocation(traceview); + + // Now the ddmui lib + DdmUiPreferences.setStore(mPrefStore); + DdmUiPreferences.setThreadRefreshInterval(mPrefStore.getInt(PREFS_THREAD_REFRESH_INTERVAL)); + } + + /* + * Set default values for all preferences. These are either defined + * statically or are based on the values set by the class initializers + * in other classes. + * + * The other threads (e.g. VMWatcherThread) haven't been created yet, + * so we want to use static values rather than reading fields from + * class.getInstance(). + */ + private static void setDefaults(String homeDir) { + mPrefStore.setDefault(PREFS_DEBUG_PORT_BASE, DdmPreferences.DEFAULT_DEBUG_PORT_BASE); + + mPrefStore.setDefault(PREFS_SELECTED_DEBUG_PORT, + DdmPreferences.DEFAULT_SELECTED_DEBUG_PORT); + + mPrefStore.setDefault(PREFS_DEFAULT_THREAD_UPDATE, true); + mPrefStore.setDefault(PREFS_DEFAULT_HEAP_UPDATE, false); + mPrefStore.setDefault(PREFS_THREAD_REFRESH_INTERVAL, + DdmUiPreferences.DEFAULT_THREAD_REFRESH_INTERVAL); + + mPrefStore.setDefault("textSaveDir", homeDir); //$NON-NLS-1$ + mPrefStore.setDefault("imageSaveDir", homeDir); //$NON-NLS-1$ + + mPrefStore.setDefault(PREFS_LOG_LEVEL, "info"); //$NON-NLS-1$ + + mPrefStore.setDefault(PREFS_TIMEOUT, DdmPreferences.DEFAULT_TIMEOUT); + + // choose a default font for the text output + FontData fdat = new FontData("Courier", 10, SWT.NORMAL); //$NON-NLS-1$ + mPrefStore.setDefault("textOutputFont", fdat.toString()); //$NON-NLS-1$ + + // layout information. + mPrefStore.setDefault(SHELL_X, 100); + mPrefStore.setDefault(SHELL_Y, 100); + mPrefStore.setDefault(SHELL_WIDTH, 800); + mPrefStore.setDefault(SHELL_HEIGHT, 600); + + mPrefStore.setDefault(EXPLORER_SHELL_X, 50); + mPrefStore.setDefault(EXPLORER_SHELL_Y, 50); + + mPrefStore.setDefault(SHOW_NATIVE_HEAP, false); + } + + + /* + * Create a "listener" to take action when preferences change. These are + * required for ongoing activities that don't check prefs on each use. + * + * This is only invoked when something explicitly changes the value of + * a preference (e.g. not when the prefs file is loaded). + */ + private static class ChangeListener implements IPropertyChangeListener { + public void propertyChange(PropertyChangeEvent event) { + String changed = event.getProperty(); + + if (changed.equals(PREFS_DEBUG_PORT_BASE)) { + DdmPreferences.setDebugPortBase(mPrefStore.getInt(PREFS_DEBUG_PORT_BASE)); + } else if (changed.equals(PREFS_SELECTED_DEBUG_PORT)) { + DdmPreferences.setSelectedDebugPort(mPrefStore.getInt(PREFS_SELECTED_DEBUG_PORT)); + } else if (changed.equals(PREFS_LOG_LEVEL)) { + DdmPreferences.setLogLevel((String)event.getNewValue()); + } else if (changed.equals("textSaveDir")) { + mPrefStore.setValue("lastTextSaveDir", + (String) event.getNewValue()); + } else if (changed.equals("imageSaveDir")) { + mPrefStore.setValue("lastImageSaveDir", + (String) event.getNewValue()); + } else if (changed.equals(PREFS_TIMEOUT)) { + DdmPreferences.setTimeOut(mPrefStore.getInt(PREFS_TIMEOUT)); + } else { + Log.v("ddms", "Preference change: " + event.getProperty() + + ": '" + event.getOldValue() + + "' --> '" + event.getNewValue() + "'"); + } + } + } + + + /** + * Create and display the dialog. + */ + public static void run(Shell shell) { + assert mPrefStore != null; + + PreferenceManager prefMgr = new PreferenceManager(); + + PreferenceNode node, subNode; + + // this didn't work -- got NPE, possibly from class lookup: + //PreferenceNode app = new PreferenceNode("app", "Application", null, + // AppPrefs.class.getName()); + + node = new PreferenceNode("debugger", new DebuggerPrefs()); + prefMgr.addToRoot(node); + + subNode = new PreferenceNode("panel", new PanelPrefs()); + //prefMgr.addTo(node.getId(), subNode); + prefMgr.addToRoot(subNode); + + node = new PreferenceNode("LogCat", new LogCatPrefs()); + prefMgr.addToRoot(node); + + node = new PreferenceNode("misc", new MiscPrefs()); + prefMgr.addToRoot(node); + + node = new PreferenceNode("stats", new UsageStatsPrefs()); + prefMgr.addToRoot(node); + + PreferenceDialog dlg = new PreferenceDialog(shell, prefMgr); + dlg.setPreferenceStore(mPrefStore); + + // run it + dlg.open(); + + // save prefs + try { + mPrefStore.save(); + } + catch (IOException ioe) { + } + + // discard the stuff we created + //prefMgr.dispose(); + //dlg.dispose(); + } + + /** + * "Debugger" prefs page. + */ + private static class DebuggerPrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public DebuggerPrefs() { + super(GRID); // use "grid" layout so edit boxes line up + setTitle("Debugger"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + IntegerFieldEditor ife; + + ife = new PortFieldEditor(PREFS_DEBUG_PORT_BASE, + "Starting value for local port:", getFieldEditorParent()); + addField(ife); + + ife = new PortFieldEditor(PREFS_SELECTED_DEBUG_PORT, + "Port of Selected VM:", getFieldEditorParent()); + addField(ife); + } + } + + /** + * "Panel" prefs page. + */ + private static class PanelPrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public PanelPrefs() { + super(FLAT); // use "flat" layout + setTitle("Info Panels"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + BooleanFieldEditor bfe; + IntegerFieldEditor ife; + + bfe = new BooleanFieldEditor(PREFS_DEFAULT_THREAD_UPDATE, + "Thread updates enabled by default", getFieldEditorParent()); + addField(bfe); + + bfe = new BooleanFieldEditor(PREFS_DEFAULT_HEAP_UPDATE, + "Heap updates enabled by default", getFieldEditorParent()); + addField(bfe); + + ife = new IntegerFieldEditor(PREFS_THREAD_REFRESH_INTERVAL, + "Thread status interval (seconds):", getFieldEditorParent()); + ife.setValidRange(1, 60); + addField(ife); + } + } + + /** + * "logcat" prefs page. + */ + private static class LogCatPrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public LogCatPrefs() { + super(FLAT); // use "flat" layout + setTitle("Logcat"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + RadioGroupFieldEditor rgfe; + + rgfe = new RadioGroupFieldEditor(PrefsDialog.LOGCAT_COLUMN_MODE, + "Message Column Resizing Mode", 1, new String[][] { + { "Manual", PrefsDialog.LOGCAT_COLUMN_MODE_MANUAL }, + { "Automatic", PrefsDialog.LOGCAT_COLUMN_MODE_AUTO }, + }, + getFieldEditorParent(), true); + addField(rgfe); + + FontFieldEditor ffe = new FontFieldEditor(PrefsDialog.LOGCAT_FONT, "Text output font:", + getFieldEditorParent()); + addField(ffe); + } + } + + /** + * "misc" prefs page. + */ + private static class MiscPrefs extends FieldEditorPreferencePage { + + /** + * Basic constructor. + */ + public MiscPrefs() { + super(FLAT); // use "flat" layout + setTitle("Misc"); + } + + /** + * Create field editors. + */ + @Override + protected void createFieldEditors() { + DirectoryFieldEditor dfe; + FontFieldEditor ffe; + + IntegerFieldEditor ife = new IntegerFieldEditor(PREFS_TIMEOUT, + "ADB connection time out (ms):", getFieldEditorParent()); + addField(ife); + + dfe = new DirectoryFieldEditor("textSaveDir", + "Default text save dir:", getFieldEditorParent()); + addField(dfe); + + dfe = new DirectoryFieldEditor("imageSaveDir", + "Default image save dir:", getFieldEditorParent()); + addField(dfe); + + ffe = new FontFieldEditor("textOutputFont", "Text output font:", + getFieldEditorParent()); + addField(ffe); + + RadioGroupFieldEditor rgfe; + + rgfe = new RadioGroupFieldEditor(PREFS_LOG_LEVEL, + "Logging Level", 1, new String[][] { + { "Verbose", LogLevel.VERBOSE.getStringValue() }, + { "Debug", LogLevel.DEBUG.getStringValue() }, + { "Info", LogLevel.INFO.getStringValue() }, + { "Warning", LogLevel.WARN.getStringValue() }, + { "Error", LogLevel.ERROR.getStringValue() }, + { "Assert", LogLevel.ASSERT.getStringValue() }, + }, + getFieldEditorParent(), true); + addField(rgfe); + } + } + + /** + * "Device" prefs page. + */ + private static class UsageStatsPrefs extends PreferencePage { + + private BooleanFieldEditor mOptInCheckbox; + private Composite mTop; + + /** + * Basic constructor. + */ + public UsageStatsPrefs() { + setTitle("Usage Stats"); + } + + @Override + protected Control createContents(Composite parent) { + mTop = new Composite(parent, SWT.NONE); + mTop.setLayout(new GridLayout(1, false)); + mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Link text = new Link(mTop, SWT.WRAP); + text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + text.setText(SdkStatsService.BODY_TEXT); + + text.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + SdkStatsService.openUrl(event.text); + } + }); + + mOptInCheckbox = new BooleanFieldEditor(SdkStatsService.PING_OPT_IN, + SdkStatsService.CHECKBOX_TEXT, mTop); + mOptInCheckbox.setPage(this); + mOptInCheckbox.setPreferenceStore(getPreferenceStore()); + mOptInCheckbox.load(); + + return null; + } + + @Override + protected Point doComputeSize() { + if (mTop != null) { + return mTop.computeSize(450, SWT.DEFAULT, true); + } + + return super.doComputeSize(); + } + + @Override + protected void performDefaults() { + if (mOptInCheckbox != null) { + mOptInCheckbox.loadDefault(); + } + super.performDefaults(); + } + + @Override + public void performApply() { + if (mOptInCheckbox != null) { + mOptInCheckbox.store(); + } + super.performApply(); + } + + @Override + public boolean performOk() { + if (mOptInCheckbox != null) { + mOptInCheckbox.store(); + } + return super.performOk(); + } + } + +} + + diff --git a/ddms/app/src/com/android/ddms/StaticPortConfigDialog.java b/ddms/app/src/com/android/ddms/StaticPortConfigDialog.java new file mode 100644 index 000000000..d00bc7f57 --- /dev/null +++ b/ddms/app/src/com/android/ddms/StaticPortConfigDialog.java @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2007 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.ddms; + +import com.android.ddmuilib.TableHelper; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Dialog to configure the static debug ports. + * + */ +public class StaticPortConfigDialog extends Dialog { + + /** Preference name for the 0th column width */ + private static final String PREFS_DEVICE_COL = "spcd.deviceColumn"; //$NON-NLS-1$ + + /** Preference name for the 1st column width */ + private static final String PREFS_APP_COL = "spcd.AppColumn"; //$NON-NLS-1$ + + /** Preference name for the 2nd column width */ + private static final String PREFS_PORT_COL = "spcd.PortColumn"; //$NON-NLS-1$ + + private static final int COL_DEVICE = 0; + private static final int COL_APPLICATION = 1; + private static final int COL_PORT = 2; + + + private static final int DLG_WIDTH = 500; + private static final int DLG_HEIGHT = 300; + + private Shell mShell; + private Shell mParent; + + private Table mPortTable; + + /** + * Array containing the list of already used static port to avoid + * duplication. + */ + private ArrayList mPorts = new ArrayList(); + + /** + * Basic constructor. + * @param parent + */ + public StaticPortConfigDialog(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + } + + /** + * Open and display the dialog. This method returns only when the + * user closes the dialog somehow. + * + */ + public void open() { + createUI(); + + if (mParent == null || mShell == null) { + return; + } + + updateFromStore(); + + // Set the dialog size. + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.pack(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + } + + /** + * Creates the dialog ui. + */ + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Static Port Configuration"); + + mShell.setLayout(new GridLayout(1, true)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + event.doit = true; + } + }); + + // center part with the list on the left and the buttons + // on the right. + Composite main = new Composite(mShell, SWT.NONE); + main.setLayoutData(new GridData(GridData.FILL_BOTH)); + main.setLayout(new GridLayout(2, false)); + + // left part: list view + mPortTable = new Table(main, SWT.SINGLE | SWT.FULL_SELECTION); + mPortTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mPortTable.setHeaderVisible(true); + mPortTable.setLinesVisible(true); + + TableHelper.createTableColumn(mPortTable, "Device Serial Number", + SWT.LEFT, "emulator-5554", //$NON-NLS-1$ + PREFS_DEVICE_COL, PrefsDialog.getStore()); + + TableHelper.createTableColumn(mPortTable, "Application Package", + SWT.LEFT, "com.android.samples.phone", //$NON-NLS-1$ + PREFS_APP_COL, PrefsDialog.getStore()); + + TableHelper.createTableColumn(mPortTable, "Debug Port", + SWT.RIGHT, "Debug Port", //$NON-NLS-1$ + PREFS_PORT_COL, PrefsDialog.getStore()); + + // right part: buttons + Composite buttons = new Composite(main, SWT.NONE); + buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + buttons.setLayout(new GridLayout(1, true)); + + Button newButton = new Button(buttons, SWT.NONE); + newButton.setText("New..."); + newButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + StaticPortEditDialog dlg = new StaticPortEditDialog(mShell, + mPorts); + if (dlg.open()) { + // get the text + String device = dlg.getDeviceSN(); + String app = dlg.getAppName(); + int port = dlg.getPortNumber(); + + // add it to the list + addEntry(device, app, port); + } + } + }); + + final Button editButton = new Button(buttons, SWT.NONE); + editButton.setText("Edit..."); + editButton.setEnabled(false); + editButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = mPortTable.getSelectionIndex(); + String oldDeviceName = getDeviceName(index); + String oldAppName = getAppName(index); + String oldPortNumber = getPortNumber(index); + StaticPortEditDialog dlg = new StaticPortEditDialog(mShell, + mPorts, oldDeviceName, oldAppName, oldPortNumber); + if (dlg.open()) { + // get the text + String deviceName = dlg.getDeviceSN(); + String app = dlg.getAppName(); + int port = dlg.getPortNumber(); + + // add it to the list + replaceEntry(index, deviceName, app, port); + } + } + }); + + final Button deleteButton = new Button(buttons, SWT.NONE); + deleteButton.setText("Delete"); + deleteButton.setEnabled(false); + deleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = mPortTable.getSelectionIndex(); + removeEntry(index); + } + }); + + // bottom part with the ok/cancel + Composite bottomComp = new Composite(mShell, SWT.NONE); + bottomComp.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_CENTER)); + bottomComp.setLayout(new GridLayout(2, true)); + + Button okButton = new Button(bottomComp, SWT.NONE); + okButton.setText("OK"); + okButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateStore(); + mShell.close(); + } + }); + + Button cancelButton = new Button(bottomComp, SWT.NONE); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + mPortTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection index + int index = mPortTable.getSelectionIndex(); + + boolean enabled = index != -1; + editButton.setEnabled(enabled); + deleteButton.setEnabled(enabled); + } + }); + + mShell.pack(); + + } + + /** + * Add a new entry in the list. + * @param deviceName the serial number of the device + * @param appName java package for the application + * @param portNumber port number + */ + private void addEntry(String deviceName, String appName, int portNumber) { + // create a new item for the table + TableItem item = new TableItem(mPortTable, SWT.NONE); + + item.setText(COL_DEVICE, deviceName); + item.setText(COL_APPLICATION, appName); + item.setText(COL_PORT, Integer.toString(portNumber)); + + // add the port to the list of port number used. + mPorts.add(portNumber); + } + + /** + * Remove an entry from the list. + * @param index The index of the entry to be removed + */ + private void removeEntry(int index) { + // remove from the ui + mPortTable.remove(index); + + // and from the port list. + mPorts.remove(index); + } + + /** + * Replace an entry in the list with new values. + * @param index The index of the item to be replaced + * @param deviceName the serial number of the device + * @param appName The new java package for the application + * @param portNumber The new port number. + */ + private void replaceEntry(int index, String deviceName, String appName, int portNumber) { + // get the table item by index + TableItem item = mPortTable.getItem(index); + + // set its new value + item.setText(COL_DEVICE, deviceName); + item.setText(COL_APPLICATION, appName); + item.setText(COL_PORT, Integer.toString(portNumber)); + + // and replace the port number in the port list. + mPorts.set(index, portNumber); + } + + + /** + * Returns the device name for a specific index + * @param index The index + * @return the java package name of the application + */ + private String getDeviceName(int index) { + TableItem item = mPortTable.getItem(index); + return item.getText(COL_DEVICE); + } + + /** + * Returns the application name for a specific index + * @param index The index + * @return the java package name of the application + */ + private String getAppName(int index) { + TableItem item = mPortTable.getItem(index); + return item.getText(COL_APPLICATION); + } + + /** + * Returns the port number for a specific index + * @param index The index + * @return the port number + */ + private String getPortNumber(int index) { + TableItem item = mPortTable.getItem(index); + return item.getText(COL_PORT); + } + + /** + * Updates the ui from the value in the preference store. + */ + private void updateFromStore() { + // get the map from the debug port manager + DebugPortProvider provider = DebugPortProvider.getInstance(); + Map> map = provider.getPortList(); + + // we're going to loop on the keys and fill the table. + Set deviceKeys = map.keySet(); + + for (String deviceKey : deviceKeys) { + Map deviceMap = map.get(deviceKey); + if (deviceMap != null) { + Set appKeys = deviceMap.keySet(); + + for (String appKey : appKeys) { + Integer port = deviceMap.get(appKey); + if (port != null) { + addEntry(deviceKey, appKey, port); + } + } + } + } + } + + /** + * Update the store from the content of the ui. + */ + private void updateStore() { + // create a new Map object and fill it. + HashMap> map = new HashMap>(); + + int count = mPortTable.getItemCount(); + + for (int i = 0 ; i < count ; i++) { + TableItem item = mPortTable.getItem(i); + String deviceName = item.getText(COL_DEVICE); + + Map deviceMap = map.get(deviceName); + if (deviceMap == null) { + deviceMap = new HashMap(); + map.put(deviceName, deviceMap); + } + + deviceMap.put(item.getText(COL_APPLICATION), Integer.valueOf(item.getText(COL_PORT))); + } + + // set it in the store through the debug port manager. + DebugPortProvider provider = DebugPortProvider.getInstance(); + provider.setPortList(map); + } +} diff --git a/ddms/app/src/com/android/ddms/StaticPortEditDialog.java b/ddms/app/src/com/android/ddms/StaticPortEditDialog.java new file mode 100644 index 000000000..b2249672e --- /dev/null +++ b/ddms/app/src/com/android/ddms/StaticPortEditDialog.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2007 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.ddms; + +import com.android.ddmlib.IDevice; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; + +/** + * Small dialog box to edit a static port number. + */ +public class StaticPortEditDialog extends Dialog { + + private static final int DLG_WIDTH = 400; + private static final int DLG_HEIGHT = 200; + + private Shell mParent; + + private Shell mShell; + + private boolean mOk = false; + + private String mAppName; + + private String mPortNumber; + + private Button mOkButton; + + private Label mWarning; + + /** List of ports already in use */ + private ArrayList mPorts; + + /** This is the port being edited. */ + private int mEditPort = -1; + private String mDeviceSn; + + /** + * Creates a dialog with empty fields. + * @param parent The parent Shell + * @param ports The list of already used port numbers. + */ + public StaticPortEditDialog(Shell parent, ArrayList ports) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mPorts = ports; + mDeviceSn = IDevice.FIRST_EMULATOR_SN; + } + + /** + * Creates a dialog with predefined values. + * @param shell The parent shell + * @param ports The list of already used port numbers. + * @param oldDeviceSN the device serial number to display + * @param oldAppName The application name to display + * @param oldPortNumber The port number to display + */ + public StaticPortEditDialog(Shell shell, ArrayList ports, + String oldDeviceSN, String oldAppName, String oldPortNumber) { + this(shell, ports); + + mDeviceSn = oldDeviceSN; + mAppName = oldAppName; + mPortNumber = oldPortNumber; + mEditPort = Integer.valueOf(mPortNumber); + } + + /** + * Opens the dialog. The method will return when the user closes the dialog + * somehow. + * + * @return true if ok was pressed, false if cancelled. + */ + public boolean open() { + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.open(); + + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mOk; + } + + public String getDeviceSN() { + return mDeviceSn; + } + + public String getAppName() { + return mAppName; + } + + public int getPortNumber() { + return Integer.valueOf(mPortNumber); + } + + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Static Port"); + + mShell.setLayout(new GridLayout(1, false)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + } + }); + + // center part with the edit field + Composite main = new Composite(mShell, SWT.NONE); + main.setLayoutData(new GridData(GridData.FILL_BOTH)); + main.setLayout(new GridLayout(2, false)); + + Label l0 = new Label(main, SWT.NONE); + l0.setText("Device Name:"); + + final Text deviceSNText = new Text(main, SWT.SINGLE | SWT.BORDER); + deviceSNText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (mDeviceSn != null) { + deviceSNText.setText(mDeviceSn); + } + deviceSNText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mDeviceSn = deviceSNText.getText().trim(); + validate(); + } + }); + + Label l = new Label(main, SWT.NONE); + l.setText("Application Name:"); + + final Text appNameText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mAppName != null) { + appNameText.setText(mAppName); + } + appNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + appNameText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mAppName = appNameText.getText().trim(); + validate(); + } + }); + + Label l2 = new Label(main, SWT.NONE); + l2.setText("Debug Port:"); + + final Text debugPortText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mPortNumber != null) { + debugPortText.setText(mPortNumber); + } + debugPortText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + debugPortText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mPortNumber = debugPortText.getText().trim(); + validate(); + } + }); + + // warning label + Composite warningComp = new Composite(mShell, SWT.NONE); + warningComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + warningComp.setLayout(new GridLayout(1, true)); + + mWarning = new Label(warningComp, SWT.NONE); + mWarning.setText(""); + mWarning.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // bottom part with the ok/cancel + Composite bottomComp = new Composite(mShell, SWT.NONE); + bottomComp + .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + bottomComp.setLayout(new GridLayout(2, true)); + + mOkButton = new Button(bottomComp, SWT.NONE); + mOkButton.setText("OK"); + mOkButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mOk = true; + mShell.close(); + } + }); + mOkButton.setEnabled(false); + mShell.setDefaultButton(mOkButton); + + Button cancelButton = new Button(bottomComp, SWT.NONE); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + validate(); + } + + /** + * Validates the content of the 2 text fields and enable/disable "ok", while + * setting up the warning/error message. + */ + private void validate() { + // first we reset the warning dialog. This allows us to latter + // display warnings. + mWarning.setText(""); // $NON-NLS-1$ + + // check the device name field is not empty + if (mDeviceSn == null || mDeviceSn.length() == 0) { + mWarning.setText("Device name missing."); + mOkButton.setEnabled(false); + return; + } + + // check the application name field is not empty + if (mAppName == null || mAppName.length() == 0) { + mWarning.setText("Application name missing."); + mOkButton.setEnabled(false); + return; + } + + String packageError = "Application name must be a valid Java package name."; + + // validate the package name as well. It must be a fully qualified + // java package. + String[] packageSegments = mAppName.split("\\."); // $NON-NLS-1$ + for (String p : packageSegments) { + if (p.matches("^[a-zA-Z][a-zA-Z0-9]*") == false) { // $NON-NLS-1$ + mWarning.setText(packageError); + mOkButton.setEnabled(false); + return; + } + + // lets also display a warning if the package contains upper case + // letters. + if (p.matches("^[a-z][a-z0-9]*") == false) { // $NON-NLS-1$ + mWarning.setText("Lower case is recommended for Java packages."); + } + } + + // the split will not detect the last char being a '.' + // so we test it manually + if (mAppName.charAt(mAppName.length()-1) == '.') { + mWarning.setText(packageError); + mOkButton.setEnabled(false); + return; + } + + // now we test the package name field is not empty. + if (mPortNumber == null || mPortNumber.length() == 0) { + mWarning.setText("Port Number missing."); + mOkButton.setEnabled(false); + return; + } + + // then we check it only contains digits. + if (mPortNumber.matches("[0-9]*") == false) { // $NON-NLS-1$ + mWarning.setText("Port Number invalid."); + mOkButton.setEnabled(false); + return; + } + + // get the int from the port number to validate + long port = Long.valueOf(mPortNumber); + if (port >= 32767) { + mOkButton.setEnabled(false); + return; + } + + // check if its in the list of already used ports + if (port != mEditPort) { + for (Integer i : mPorts) { + if (port == i.intValue()) { + mWarning.setText("Port already in use."); + mOkButton.setEnabled(false); + return; + } + } + } + + // at this point there's not error, so we enable the ok button. + mOkButton.setEnabled(true); + } +} diff --git a/ddms/app/src/com/android/ddms/UIThread.java b/ddms/app/src/com/android/ddms/UIThread.java new file mode 100644 index 000000000..7940c7429 --- /dev/null +++ b/ddms/app/src/com/android/ddms/UIThread.java @@ -0,0 +1,1703 @@ +/* + * Copyright (C) 2007 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.ddms; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.ClientData.IHprofDumpHandler; +import com.android.ddmlib.ClientData.MethodProfilingStatus; +import com.android.ddmlib.Log.ILogOutput; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.SyncService.SyncResult; +import com.android.ddmuilib.AllocationPanel; +import com.android.ddmuilib.DevicePanel; +import com.android.ddmuilib.EmulatorControlPanel; +import com.android.ddmuilib.HeapPanel; +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.ImageHelper; +import com.android.ddmuilib.ImageLoader; +import com.android.ddmuilib.InfoPanel; +import com.android.ddmuilib.NativeHeapPanel; +import com.android.ddmuilib.ScreenShotDialog; +import com.android.ddmuilib.SysinfoPanel; +import com.android.ddmuilib.TablePanel; +import com.android.ddmuilib.ThreadPanel; +import com.android.ddmuilib.DevicePanel.IUiSelectionListener; +import com.android.ddmuilib.actions.ToolItemAction; +import com.android.ddmuilib.explorer.DeviceExplorer; +import com.android.ddmuilib.handler.BaseFileHandler; +import com.android.ddmuilib.handler.MethodProfilingHandler; +import com.android.ddmuilib.log.event.EventLogPanel; +import com.android.ddmuilib.logcat.LogColors; +import com.android.ddmuilib.logcat.LogFilter; +import com.android.ddmuilib.logcat.LogPanel; +import com.android.ddmuilib.logcat.LogPanel.ILogFilterStorageManager; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTError; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.MenuAdapter; +import org.eclipse.swt.events.MenuEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.events.ShellListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; + +import java.io.File; +import java.util.ArrayList; + +/** + * This acts as the UI builder. This cannot be its own thread since this prevent using AWT in an + * SWT application. So this class mainly builds the ui, and manages communication between the panels + * when {@link IDevice} / {@link Client} selection changes. + */ +public class UIThread implements IUiSelectionListener, IClientChangeListener { + /* + * UI tab panel definitions. The constants here must match up with the array + * indices in mPanels. PANEL_CLIENT_LIST is a "virtual" panel representing + * the client list. + */ + public static final int PANEL_CLIENT_LIST = -1; + + public static final int PANEL_INFO = 0; + + public static final int PANEL_THREAD = 1; + + public static final int PANEL_HEAP = 2; + + public static final int PANEL_NATIVE_HEAP = 3; + + private static final int PANEL_ALLOCATIONS = 4; + + private static final int PANEL_SYSINFO = 5; + + private static final int PANEL_COUNT = 6; + + /** Content is setup in the constructor */ + private static TablePanel[] mPanels = new TablePanel[PANEL_COUNT]; + + private static final String[] mPanelNames = new String[] { + "Info", "Threads", "VM Heap", "Native Heap", "Allocation Tracker", "Sysinfo" + }; + + private static final String[] mPanelTips = new String[] { + "Client information", "Thread status", "VM heap status", + "Native heap status", "Allocation Tracker", "Sysinfo graphs" + }; + + private static final String PREFERENCE_LOGSASH = + "logSashLocation"; //$NON-NLS-1$ + private static final String PREFERENCE_SASH = + "sashLocation"; //$NON-NLS-1$ + + private static final String PREFS_COL_TIME = + "logcat.time"; //$NON-NLS-1$ + private static final String PREFS_COL_LEVEL = + "logcat.level"; //$NON-NLS-1$ + private static final String PREFS_COL_PID = + "logcat.pid"; //$NON-NLS-1$ + private static final String PREFS_COL_TAG = + "logcat.tag"; //$NON-NLS-1$ + private static final String PREFS_COL_MESSAGE = + "logcat.message"; //$NON-NLS-1$ + + private static final String PREFS_FILTERS = "logcat.filter"; //$NON-NLS-1$ + + // singleton instance + private static UIThread mInstance = new UIThread(); + + // our display + private Display mDisplay; + + // the table we show in the left-hand pane + private DevicePanel mDevicePanel; + + private IDevice mCurrentDevice = null; + private Client mCurrentClient = null; + + // status line at the bottom of the app window + private Label mStatusLine; + + // some toolbar items we need to update + private ToolItem mTBShowThreadUpdates; + private ToolItem mTBShowHeapUpdates; + private ToolItem mTBHalt; + private ToolItem mTBCauseGc; + private ToolItem mTBDumpHprof; + private ToolItem mTBProfiling; + + private ImageLoader mDdmsImageLoader; + private ImageLoader mDdmuiLibImageLoader; + + private final class FilterStorage implements ILogFilterStorageManager { + + public LogFilter[] getFilterFromStore() { + String filterPrefs = PrefsDialog.getStore().getString( + PREFS_FILTERS); + + // split in a string per filter + String[] filters = filterPrefs.split("\\|"); //$NON-NLS-1$ + + ArrayList list = + new ArrayList(filters.length); + + for (String f : filters) { + if (f.length() > 0) { + LogFilter logFilter = new LogFilter(); + if (logFilter.loadFromString(f)) { + list.add(logFilter); + } + } + } + + return list.toArray(new LogFilter[list.size()]); + } + + public void saveFilters(LogFilter[] filters) { + StringBuilder sb = new StringBuilder(); + for (LogFilter f : filters) { + String filterString = f.toString(); + sb.append(filterString); + sb.append('|'); + } + + PrefsDialog.getStore().setValue(PREFS_FILTERS, sb.toString()); + } + + public boolean requiresDefaultFilter() { + return true; + } + } + + + private LogPanel mLogPanel; + + private ToolItemAction mCreateFilterAction; + private ToolItemAction mDeleteFilterAction; + private ToolItemAction mEditFilterAction; + private ToolItemAction mExportAction; + private ToolItemAction mClearAction; + + private ToolItemAction[] mLogLevelActions; + private String[] mLogLevelIcons = { + "v.png", //$NON-NLS-1S + "d.png", //$NON-NLS-1S + "i.png", //$NON-NLS-1S + "w.png", //$NON-NLS-1S + "e.png", //$NON-NLS-1S + }; + + protected Clipboard mClipboard; + + private MenuItem mCopyMenuItem; + + private MenuItem mSelectAllMenuItem; + + private TableFocusListener mTableListener; + + private DeviceExplorer mExplorer = null; + private Shell mExplorerShell = null; + + private EmulatorControlPanel mEmulatorPanel; + + private EventLogPanel mEventLogPanel; + + private Image mTracingStartImage; + + private Image mTracingStopImage; + + private class TableFocusListener implements ITableFocusListener { + + private IFocusedTableActivator mCurrentActivator; + + public void focusGained(IFocusedTableActivator activator) { + mCurrentActivator = activator; + if (mCopyMenuItem.isDisposed() == false) { + mCopyMenuItem.setEnabled(true); + mSelectAllMenuItem.setEnabled(true); + } + } + + public void focusLost(IFocusedTableActivator activator) { + // if we move from one table to another, it's unclear + // if the old table lose its focus before the new + // one gets the focus, so we need to check. + if (activator == mCurrentActivator) { + activator = null; + if (mCopyMenuItem.isDisposed() == false) { + mCopyMenuItem.setEnabled(false); + mSelectAllMenuItem.setEnabled(false); + } + } + } + + public void copy(Clipboard clipboard) { + if (mCurrentActivator != null) { + mCurrentActivator.copy(clipboard); + } + } + + public void selectAll() { + if (mCurrentActivator != null) { + mCurrentActivator.selectAll(); + } + } + } + + /** + * Handler for HPROF dumps. + * This will always prompt the user to save the HPROF file. + */ + private class HProfHandler extends BaseFileHandler implements IHprofDumpHandler { + + public HProfHandler(Shell parentShell) { + super(parentShell); + } + + public void onEndFailure(final Client client, final String message) { + mDisplay.asyncExec(new Runnable() { + public void run() { + try { + displayErrorFromUiThread( + "Unable to create HPROF file for application '%1$s'\n\n%2$s" + + "Check logcat for more information.", + client.getClientData().getClientDescription(), + message != null ? message + "\n\n" : ""); + } finally { + // this will make sure the dump hprof button is re-enabled for the + // current selection. as the client is finished dumping an hprof file + enableButtons(); + } + } + }); + } + + public void onSuccess(final String remoteFilePath, final Client client) { + mDisplay.asyncExec(new Runnable() { + public void run() { + final IDevice device = client.getDevice(); + try { + // get the sync service to pull the HPROF file + final SyncService sync = client.getDevice().getSyncService(); + if (sync != null) { + SyncResult result = promptAndPull(sync, + client.getClientData().getClientDescription() + ".hprof", + remoteFilePath, "Save HPROF file"); + if (result != null && result.getCode() != SyncService.RESULT_OK) { + displayErrorFromUiThread( + "Unable to download HPROF file from device '%1$s'.\n\n%2$s", + device.getSerialNumber(), result.getMessage()); + } + } else { + displayErrorFromUiThread("Unable to download HPROF file from device '%1$s'.", + device.getSerialNumber()); + } + } catch (Exception e) { + displayErrorFromUiThread("Unable to download HPROF file from device '%1$s'.", + device.getSerialNumber()); + + } finally { + // this will make sure the dump hprof button is re-enabled for the + // current selection. as the client is finished dumping an hprof file + enableButtons(); + } + } + }); + } + + public void onSuccess(final byte[] data, final Client client) { + mDisplay.asyncExec(new Runnable() { + public void run() { + promptAndSave(client.getClientData().getClientDescription() + ".hprof", data, + "Save HPROF file"); + } + }); + } + + @Override + protected String getDialogTitle() { + return "HPROF Error"; + } + } + + + /** + * Generic constructor. + */ + private UIThread() { + mPanels[PANEL_INFO] = new InfoPanel(); + mPanels[PANEL_THREAD] = new ThreadPanel(); + mPanels[PANEL_HEAP] = new HeapPanel(); + if (PrefsDialog.getStore().getBoolean(PrefsDialog.SHOW_NATIVE_HEAP)) { + mPanels[PANEL_NATIVE_HEAP] = new NativeHeapPanel(); + } else { + mPanels[PANEL_NATIVE_HEAP] = null; + } + mPanels[PANEL_ALLOCATIONS] = new AllocationPanel(); + mPanels[PANEL_SYSINFO] = new SysinfoPanel(); + } + + /** + * Get singleton instance of the UI thread. + */ + public static UIThread getInstance() { + return mInstance; + } + + /** + * Return the Display. Don't try this unless you're in the UI thread. + */ + public Display getDisplay() { + return mDisplay; + } + + public void asyncExec(Runnable r) { + if (mDisplay != null && mDisplay.isDisposed() == false) { + mDisplay.asyncExec(r); + } + } + + /** returns the IPreferenceStore */ + public IPreferenceStore getStore() { + return PrefsDialog.getStore(); + } + + /** + * Create SWT objects and drive the user interface event loop. + * @param location location of the folder that contains ddms. + */ + public void runUI(String ddmsParentLocation) { + Display.setAppName("ddms"); + mDisplay = new Display(); + final Shell shell = new Shell(mDisplay); + + // create the image loaders for DDMS and DDMUILIB + mDdmsImageLoader = new ImageLoader(this.getClass()); + mDdmuiLibImageLoader = new ImageLoader(DevicePanel.class); + + shell.setImage(ImageHelper.loadImage(mDdmsImageLoader, mDisplay, + "ddms-icon.png", //$NON-NLS-1$ + 100, 50, null)); + + Log.setLogOutput(new ILogOutput() { + public void printAndPromptLog(final LogLevel logLevel, final String tag, + final String message) { + Log.printLog(logLevel, tag, message); + // dialog box only run in UI thread.. + mDisplay.asyncExec(new Runnable() { + public void run() { + Shell shell = mDisplay.getActiveShell(); + if (logLevel == LogLevel.ERROR) { + MessageDialog.openError(shell, tag, message); + } else { + MessageDialog.openWarning(shell, tag, message); + } + } + }); + } + + public void printLog(LogLevel logLevel, String tag, String message) { + Log.printLog(logLevel, tag, message); + } + }); + + // set the handler for hprof dump + ClientData.setHprofDumpHandler(new HProfHandler(shell)); + ClientData.setMethodProfilingHandler(new MethodProfilingHandler(shell)); + + // [try to] ensure ADB is running + String adbLocation; + if (ddmsParentLocation != null && ddmsParentLocation.length() != 0) { + adbLocation = ddmsParentLocation + File.separator + "adb"; //$NON-NLS-1$ + } else { + adbLocation = "adb"; //$NON-NLS-1$ + } + + AndroidDebugBridge.init(true /* debugger support */); + AndroidDebugBridge.createBridge(adbLocation, true /* forceNewBridge */); + + // we need to listen to client change to be notified of client status (profiling) change + AndroidDebugBridge.addClientChangeListener(this); + + shell.setText("Dalvik Debug Monitor"); + setConfirmClose(shell); + createMenus(shell); + createWidgets(shell); + + shell.pack(); + setSizeAndPosition(shell); + shell.open(); + + Log.d("ddms", "UI is up"); + + while (!shell.isDisposed()) { + if (!mDisplay.readAndDispatch()) + mDisplay.sleep(); + } + mLogPanel.stopLogCat(true); + + mDevicePanel.dispose(); + for (TablePanel panel : mPanels) { + if (panel != null) { + panel.dispose(); + } + } + + mDisplay.dispose(); + Log.d("ddms", "UI is down"); + } + + /** + * Set the size and position of the main window from the preference, and + * setup listeners for control events (resize/move of the window) + */ + private void setSizeAndPosition(final Shell shell) { + shell.setMinimumSize(400, 200); + + // get the x/y and w/h from the prefs + PreferenceStore prefs = PrefsDialog.getStore(); + int x = prefs.getInt(PrefsDialog.SHELL_X); + int y = prefs.getInt(PrefsDialog.SHELL_Y); + int w = prefs.getInt(PrefsDialog.SHELL_WIDTH); + int h = prefs.getInt(PrefsDialog.SHELL_HEIGHT); + + // check that we're not out of the display area + Rectangle rect = mDisplay.getClientArea(); + // first check the width/height + if (w > rect.width) { + w = rect.width; + prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width); + } + if (h > rect.height) { + h = rect.height; + prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.height); + } + // then check x. Make sure the left corner is in the screen + if (x < rect.x) { + x = rect.x; + prefs.setValue(PrefsDialog.SHELL_X, rect.x); + } else if (x >= rect.x + rect.width) { + x = rect.x + rect.width - w; + prefs.setValue(PrefsDialog.SHELL_X, rect.x); + } + // then check y. Make sure the left corner is in the screen + if (y < rect.y) { + y = rect.y; + prefs.setValue(PrefsDialog.SHELL_Y, rect.y); + } else if (y >= rect.y + rect.height) { + y = rect.y + rect.height - h; + prefs.setValue(PrefsDialog.SHELL_Y, rect.y); + } + + // now we can set the location/size + shell.setBounds(x, y, w, h); + + // add listener for resize/move + shell.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + // get the new x/y + Rectangle rect = shell.getBounds(); + // store in pref file + PreferenceStore prefs = PrefsDialog.getStore(); + prefs.setValue(PrefsDialog.SHELL_X, rect.x); + prefs.setValue(PrefsDialog.SHELL_Y, rect.y); + } + + public void controlResized(ControlEvent e) { + // get the new w/h + Rectangle rect = shell.getBounds(); + // store in pref file + PreferenceStore prefs = PrefsDialog.getStore(); + prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width); + prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.height); + } + }); + } + + /** + * Set the size and position of the file explorer window from the + * preference, and setup listeners for control events (resize/move of + * the window) + */ + private void setExplorerSizeAndPosition(final Shell shell) { + shell.setMinimumSize(400, 200); + + // get the x/y and w/h from the prefs + PreferenceStore prefs = PrefsDialog.getStore(); + int x = prefs.getInt(PrefsDialog.EXPLORER_SHELL_X); + int y = prefs.getInt(PrefsDialog.EXPLORER_SHELL_Y); + int w = prefs.getInt(PrefsDialog.EXPLORER_SHELL_WIDTH); + int h = prefs.getInt(PrefsDialog.EXPLORER_SHELL_HEIGHT); + + // check that we're not out of the display area + Rectangle rect = mDisplay.getClientArea(); + // first check the width/height + if (w > rect.width) { + w = rect.width; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width); + } + if (h > rect.height) { + h = rect.height; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height); + } + // then check x. Make sure the left corner is in the screen + if (x < rect.x) { + x = rect.x; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x); + } else if (x >= rect.x + rect.width) { + x = rect.x + rect.width - w; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x); + } + // then check y. Make sure the left corner is in the screen + if (y < rect.y) { + y = rect.y; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y); + } else if (y >= rect.y + rect.height) { + y = rect.y + rect.height - h; + prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y); + } + + // now we can set the location/size + shell.setBounds(x, y, w, h); + + // add listener for resize/move + shell.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + // get the new x/y + Rectangle rect = shell.getBounds(); + // store in pref file + PreferenceStore prefs = PrefsDialog.getStore(); + prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x); + prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y); + } + + public void controlResized(ControlEvent e) { + // get the new w/h + Rectangle rect = shell.getBounds(); + // store in pref file + PreferenceStore prefs = PrefsDialog.getStore(); + prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width); + prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height); + } + }); + } + + /* + * Set the confirm-before-close dialog. TODO: enable/disable in prefs. TODO: + * is there any point in having this? + */ + private void setConfirmClose(final Shell shell) { + if (true) + return; + + shell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + int style = SWT.APPLICATION_MODAL | SWT.YES | SWT.NO; + MessageBox msgBox = new MessageBox(shell, style); + msgBox.setText("Confirm..."); + msgBox.setMessage("Close DDM?"); + event.doit = (msgBox.open() == SWT.YES); + } + }); + } + + /* + * Create the menu bar and items. + */ + private void createMenus(final Shell shell) { + // create menu bar + Menu menuBar = new Menu(shell, SWT.BAR); + + // create top-level items + MenuItem fileItem = new MenuItem(menuBar, SWT.CASCADE); + fileItem.setText("&File"); + MenuItem editItem = new MenuItem(menuBar, SWT.CASCADE); + editItem.setText("&Edit"); + MenuItem actionItem = new MenuItem(menuBar, SWT.CASCADE); + actionItem.setText("&Actions"); + MenuItem deviceItem = new MenuItem(menuBar, SWT.CASCADE); + deviceItem.setText("&Device"); + MenuItem helpItem = new MenuItem(menuBar, SWT.CASCADE); + helpItem.setText("&Help"); + + // create top-level menus + Menu fileMenu = new Menu(menuBar); + fileItem.setMenu(fileMenu); + Menu editMenu = new Menu(menuBar); + editItem.setMenu(editMenu); + Menu actionMenu = new Menu(menuBar); + actionItem.setMenu(actionMenu); + Menu deviceMenu = new Menu(menuBar); + deviceItem.setMenu(deviceMenu); + Menu helpMenu = new Menu(menuBar); + helpItem.setMenu(helpMenu); + + MenuItem item; + + // create File menu items + item = new MenuItem(fileMenu, SWT.NONE); + item.setText("&Preferences..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + PrefsDialog.run(shell); + } + }); + + item = new MenuItem(fileMenu, SWT.NONE); + item.setText("&Static Port Configuration..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + StaticPortConfigDialog dlg = new StaticPortConfigDialog(shell); + dlg.open(); + } + }); + + new MenuItem(fileMenu, SWT.SEPARATOR); + + item = new MenuItem(fileMenu, SWT.NONE); + item.setText("E&xit\tCtrl-Q"); + item.setAccelerator('Q' | SWT.CONTROL); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + shell.close(); + } + }); + + // create edit menu items + mCopyMenuItem = new MenuItem(editMenu, SWT.NONE); + mCopyMenuItem.setText("&Copy\tCtrl-C"); + mCopyMenuItem.setAccelerator('C' | SWT.COMMAND); + mCopyMenuItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mTableListener.copy(mClipboard); + } + }); + + new MenuItem(editMenu, SWT.SEPARATOR); + + mSelectAllMenuItem = new MenuItem(editMenu, SWT.NONE); + mSelectAllMenuItem.setText("Select &All\tCtrl-A"); + mSelectAllMenuItem.setAccelerator('A' | SWT.COMMAND); + mSelectAllMenuItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mTableListener.selectAll(); + } + }); + + // create Action menu items + // TODO: this should come with a confirmation dialog + final MenuItem actionHaltItem = new MenuItem(actionMenu, SWT.NONE); + actionHaltItem.setText("&Halt VM"); + actionHaltItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.killSelectedClient(); + } + }); + + final MenuItem actionCauseGcItem = new MenuItem(actionMenu, SWT.NONE); + actionCauseGcItem.setText("Cause &GC"); + actionCauseGcItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.forceGcOnSelectedClient(); + } + }); + + // configure Action items based on current state + actionMenu.addMenuListener(new MenuAdapter() { + @Override + public void menuShown(MenuEvent e) { + actionHaltItem.setEnabled(mTBHalt.getEnabled() && mCurrentClient != null); + actionCauseGcItem.setEnabled(mTBCauseGc.getEnabled() && mCurrentClient != null); + } + }); + + // create Device menu items + final MenuItem screenShotItem = new MenuItem(deviceMenu, SWT.NONE); + screenShotItem.setText("&Screen capture...\tCTrl-S"); + screenShotItem.setAccelerator('S' | SWT.CONTROL); + screenShotItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentDevice != null) { + ScreenShotDialog dlg = new ScreenShotDialog(shell); + dlg.open(mCurrentDevice); + } + } + }); + + new MenuItem(deviceMenu, SWT.SEPARATOR); + + final MenuItem explorerItem = new MenuItem(deviceMenu, SWT.NONE); + explorerItem.setText("File Explorer..."); + explorerItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + createFileExplorer(); + } + }); + + new MenuItem(deviceMenu, SWT.SEPARATOR); + + final MenuItem processItem = new MenuItem(deviceMenu, SWT.NONE); + processItem.setText("Show &process status..."); + processItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog("ps -x", "ps-x.txt", shell); + dlg.open(mCurrentDevice); + } + }); + + final MenuItem deviceStateItem = new MenuItem(deviceMenu, SWT.NONE); + deviceStateItem.setText("Dump &device state..."); + deviceStateItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog("/system/bin/dumpstate /proc/self/fd/0", + "device-state.txt", shell); + dlg.open(mCurrentDevice); + } + }); + + final MenuItem appStateItem = new MenuItem(deviceMenu, SWT.NONE); + appStateItem.setText("Dump &app state..."); + appStateItem.setEnabled(false); + appStateItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog("dumpsys", "app-state.txt", shell); + dlg.open(mCurrentDevice); + } + }); + + final MenuItem radioStateItem = new MenuItem(deviceMenu, SWT.NONE); + radioStateItem.setText("Dump &radio state..."); + radioStateItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog( + "cat /data/logs/radio.4 /data/logs/radio.3" + + " /data/logs/radio.2 /data/logs/radio.1" + + " /data/logs/radio", + "radio-state.txt", shell); + dlg.open(mCurrentDevice); + } + }); + + final MenuItem logCatItem = new MenuItem(deviceMenu, SWT.NONE); + logCatItem.setText("Run &logcat..."); + logCatItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + DeviceCommandDialog dlg; + dlg = new DeviceCommandDialog("logcat '*:d jdwp:w'", "log.txt", + shell); + dlg.open(mCurrentDevice); + } + }); + + // configure Action items based on current state + deviceMenu.addMenuListener(new MenuAdapter() { + @Override + public void menuShown(MenuEvent e) { + boolean deviceEnabled = mCurrentDevice != null; + screenShotItem.setEnabled(deviceEnabled); + explorerItem.setEnabled(deviceEnabled); + processItem.setEnabled(deviceEnabled); + deviceStateItem.setEnabled(deviceEnabled); + appStateItem.setEnabled(deviceEnabled); + radioStateItem.setEnabled(deviceEnabled); + logCatItem.setEnabled(deviceEnabled); + } + }); + + // create Help menu items + item = new MenuItem(helpMenu, SWT.NONE); + item.setText("&Contents..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int style = SWT.APPLICATION_MODAL | SWT.OK; + MessageBox msgBox = new MessageBox(shell, style); + msgBox.setText("Help!"); + msgBox.setMessage("Help wanted."); + msgBox.open(); + } + }); + + new MenuItem(helpMenu, SWT.SEPARATOR); + + item = new MenuItem(helpMenu, SWT.NONE); + item.setText("&About..."); + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + AboutDialog dlg = new AboutDialog(shell); + dlg.open(); + } + }); + + // tell the shell to use this menu + shell.setMenuBar(menuBar); + } + + /* + * Create the widgets in the main application window. The basic layout is a + * two-panel sash, with a scrolling list of VMs on the left and detailed + * output for a single VM on the right. + */ + private void createWidgets(final Shell shell) { + Color darkGray = shell.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + + /* + * Create three areas: tool bar, split panels, status line + */ + shell.setLayout(new GridLayout(1, false)); + + // 1. panel area + final Composite panelArea = new Composite(shell, SWT.BORDER); + + // make the panel area absorb all space + panelArea.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // 2. status line. + mStatusLine = new Label(shell, SWT.NONE); + + // make status line extend all the way across + mStatusLine.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mStatusLine.setText("Initializing..."); + + /* + * Configure the split-panel area. + */ + final PreferenceStore prefs = PrefsDialog.getStore(); + + Composite topPanel = new Composite(panelArea, SWT.NONE); + final Sash sash = new Sash(panelArea, SWT.HORIZONTAL); + sash.setBackground(darkGray); + Composite bottomPanel = new Composite(panelArea, SWT.NONE); + + panelArea.setLayout(new FormLayout()); + + createTopPanel(topPanel, darkGray); + createBottomPanel(bottomPanel); + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + topPanel.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFERENCE_LOGSASH)) { + sashData.top = new FormAttachment(0, prefs.getInt( + PREFERENCE_LOGSASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + bottomPanel.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = panelArea.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + prefs.setValue(PREFERENCE_LOGSASH, e.y); + panelArea.layout(); + } + } + }); + + // add a global focus listener for all the tables + mTableListener = new TableFocusListener(); + + // now set up the listener in the various panels + mLogPanel.setTableFocusListener(mTableListener); + mEventLogPanel.setTableFocusListener(mTableListener); + for (TablePanel p : mPanels) { + if (p != null) { + p.setTableFocusListener(mTableListener); + } + } + + mStatusLine.setText(""); + } + + /* + * Populate the tool bar. + */ + private void createDevicePanelToolBar(ToolBar toolBar) { + Display display = toolBar.getDisplay(); + + // add "show heap updates" button + mTBShowHeapUpdates = new ToolItem(toolBar, SWT.CHECK); + mTBShowHeapUpdates.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_HEAP, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBShowHeapUpdates.setToolTipText("Show heap updates"); + mTBShowHeapUpdates.setEnabled(false); + mTBShowHeapUpdates.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentClient != null) { + // boolean status = ((ToolItem)e.item).getSelection(); + // invert previous state + boolean enable = !mCurrentClient.isHeapUpdateEnabled(); + mCurrentClient.setHeapUpdateEnabled(enable); + } else { + e.doit = false; // this has no effect? + } + } + }); + + // add "dump HPROF" button + mTBDumpHprof = new ToolItem(toolBar, SWT.PUSH); + mTBDumpHprof.setToolTipText("Dump HPROF file"); + mTBDumpHprof.setEnabled(false); + mTBDumpHprof.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_HPROF, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBDumpHprof.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.dumpHprof(); + + // this will make sure the dump hprof button is disabled for the current selection + // as the client is already dumping an hprof file + enableButtons(); + } + }); + + // add "cause GC" button + mTBCauseGc = new ToolItem(toolBar, SWT.PUSH); + mTBCauseGc.setToolTipText("Cause an immediate GC"); + mTBCauseGc.setEnabled(false); + mTBCauseGc.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_GC, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBCauseGc.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.forceGcOnSelectedClient(); + } + }); + + new ToolItem(toolBar, SWT.SEPARATOR); + + // add "show thread updates" button + mTBShowThreadUpdates = new ToolItem(toolBar, SWT.CHECK); + mTBShowThreadUpdates.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_THREAD, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBShowThreadUpdates.setToolTipText("Show thread updates"); + mTBShowThreadUpdates.setEnabled(false); + mTBShowThreadUpdates.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentClient != null) { + // boolean status = ((ToolItem)e.item).getSelection(); + // invert previous state + boolean enable = !mCurrentClient.isThreadUpdateEnabled(); + + mCurrentClient.setThreadUpdateEnabled(enable); + } else { + e.doit = false; // this has no effect? + } + } + }); + + // add a start/stop method tracing + mTracingStartImage = ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_TRACING_START, + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null); + mTracingStopImage = ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_TRACING_STOP, + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null); + mTBProfiling = new ToolItem(toolBar, SWT.PUSH); + mTBProfiling.setToolTipText("Start Method Profiling"); + mTBProfiling.setEnabled(false); + mTBProfiling.setImage(mTracingStartImage); + mTBProfiling.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.toggleMethodProfiling(); + } + }); + + new ToolItem(toolBar, SWT.SEPARATOR); + + // add "kill VM" button; need to make this visually distinct from + // the status update buttons + mTBHalt = new ToolItem(toolBar, SWT.PUSH); + mTBHalt.setToolTipText("Halt the target VM"); + mTBHalt.setEnabled(false); + mTBHalt.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, display, + DevicePanel.ICON_HALT, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mTBHalt.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mDevicePanel.killSelectedClient(); + } + }); + + toolBar.pack(); + } + + private void createTopPanel(final Composite comp, Color darkGray) { + final PreferenceStore prefs = PrefsDialog.getStore(); + + comp.setLayout(new FormLayout()); + + Composite leftPanel = new Composite(comp, SWT.NONE); + final Sash sash = new Sash(comp, SWT.VERTICAL); + sash.setBackground(darkGray); + Composite rightPanel = new Composite(comp, SWT.NONE); + + createLeftPanel(leftPanel); + createRightPanel(rightPanel); + + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(sash, 0); + leftPanel.setLayoutData(data); + + final FormData sashData = new FormData(); + sashData.top = new FormAttachment(0, 0); + sashData.bottom = new FormAttachment(100, 0); + if (prefs != null && prefs.contains(PREFERENCE_SASH)) { + sashData.left = new FormAttachment(0, prefs.getInt( + PREFERENCE_SASH)); + } else { + // position the sash 380 from the right instead of x% (done by using + // FormAttachment(x, 0)) in order to keep the sash at the same + // position + // from the left when the window is resized. + // 380px is just enough to display the left table with no horizontal + // scrollbar with the default font. + sashData.left = new FormAttachment(0, 380); + } + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(sash, 0); + data.right = new FormAttachment(100, 0); + rightPanel.setLayoutData(data); + + final int minPanelWidth = 60; + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = comp.getClientArea(); + int right = panelRect.width - sashRect.width - minPanelWidth; + e.x = Math.max(Math.min(e.x, right), minPanelWidth); + if (e.x != sashRect.x) { + sashData.left = new FormAttachment(0, e.x); + prefs.setValue(PREFERENCE_SASH, e.x); + comp.layout(); + } + } + }); + } + + private void createBottomPanel(final Composite comp) { + final PreferenceStore prefs = PrefsDialog.getStore(); + + // create clipboard + Display display = comp.getDisplay(); + mClipboard = new Clipboard(display); + + LogColors colors = new LogColors(); + + colors.infoColor = new Color(display, 0, 127, 0); + colors.debugColor = new Color(display, 0, 0, 127); + colors.errorColor = new Color(display, 255, 0, 0); + colors.warningColor = new Color(display, 255, 127, 0); + colors.verboseColor = new Color(display, 0, 0, 0); + + // set the preferences names + LogPanel.PREFS_TIME = PREFS_COL_TIME; + LogPanel.PREFS_LEVEL = PREFS_COL_LEVEL; + LogPanel.PREFS_PID = PREFS_COL_PID; + LogPanel.PREFS_TAG = PREFS_COL_TAG; + LogPanel.PREFS_MESSAGE = PREFS_COL_MESSAGE; + + comp.setLayout(new GridLayout(1, false)); + + ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL); + + mCreateFilterAction = new ToolItemAction(toolBar, SWT.PUSH); + mCreateFilterAction.item.setToolTipText("Create Filter"); + mCreateFilterAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "add.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mCreateFilterAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.addFilter(); + } + }); + + mEditFilterAction = new ToolItemAction(toolBar, SWT.PUSH); + mEditFilterAction.item.setToolTipText("Edit Filter"); + mEditFilterAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "edit.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mEditFilterAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.editFilter(); + } + }); + + mDeleteFilterAction = new ToolItemAction(toolBar, SWT.PUSH); + mDeleteFilterAction.item.setToolTipText("Delete Filter"); + mDeleteFilterAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "delete.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mDeleteFilterAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.deleteFilter(); + } + }); + + + new ToolItem(toolBar, SWT.SEPARATOR); + + LogLevel[] levels = LogLevel.values(); + mLogLevelActions = new ToolItemAction[mLogLevelIcons.length]; + for (int i = 0 ; i < mLogLevelActions.length; i++) { + String name = levels[i].getStringValue(); + final ToolItemAction newAction = new ToolItemAction(toolBar, SWT.CHECK); + mLogLevelActions[i] = newAction; + //newAction.item.setText(name); + newAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // disable the other actions and record current index + for (int i = 0 ; i < mLogLevelActions.length; i++) { + ToolItemAction a = mLogLevelActions[i]; + if (a == newAction) { + a.setChecked(true); + + // set the log level + mLogPanel.setCurrentFilterLogLevel(i+2); + } else { + a.setChecked(false); + } + } + } + }); + + newAction.item.setToolTipText(name); + newAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + mLogLevelIcons[i], + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + } + + new ToolItem(toolBar, SWT.SEPARATOR); + + mClearAction = new ToolItemAction(toolBar, SWT.PUSH); + mClearAction.item.setToolTipText("Clear Log"); + + mClearAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "clear.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mClearAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.clear(); + } + }); + + new ToolItem(toolBar, SWT.SEPARATOR); + + mExportAction = new ToolItemAction(toolBar, SWT.PUSH); + mExportAction.item.setToolTipText("Export Selection As Text..."); + mExportAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, mDisplay, + "save.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + mExportAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mLogPanel.save(); + } + }); + + + toolBar.pack(); + + // now create the log view + mLogPanel = new LogPanel(new ImageLoader(LogPanel.class), colors, new FilterStorage(), + LogPanel.FILTER_MANUAL); + + mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions); + + String colMode = prefs.getString(PrefsDialog.LOGCAT_COLUMN_MODE); + if (PrefsDialog.LOGCAT_COLUMN_MODE_AUTO.equals(colMode)) { + mLogPanel.setColumnMode(LogPanel.COLUMN_MODE_AUTO); + } + + String fontStr = PrefsDialog.getStore().getString(PrefsDialog.LOGCAT_FONT); + if (fontStr != null) { + try { + FontData fdat = new FontData(fontStr); + mLogPanel.setFont(new Font(display, fdat)); + } catch (IllegalArgumentException e) { + // Looks like fontStr isn't a valid font representation. + // We do nothing in this case, the logcat view will use the default font. + } catch (SWTError e2) { + // Looks like the Font() constructor failed. + // We do nothing in this case, the logcat view will use the default font. + } + } + + mLogPanel.createPanel(comp); + + // and start the logcat + mLogPanel.startLogCat(mCurrentDevice); + } + + /* + * Create the contents of the left panel: a table of VMs. + */ + private void createLeftPanel(final Composite comp) { + comp.setLayout(new GridLayout(1, false)); + ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL | SWT.RIGHT | SWT.WRAP); + toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + createDevicePanelToolBar(toolBar); + + Composite c = new Composite(comp, SWT.NONE); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mDevicePanel = new DevicePanel(new ImageLoader(DevicePanel.class), true /* showPorts */); + mDevicePanel.createPanel(c); + + // add ourselves to the device panel selection listener + mDevicePanel.addSelectionListener(this); + } + + /* + * Create the contents of the right panel: tabs with VM information. + */ + private void createRightPanel(final Composite comp) { + TabItem item; + TabFolder tabFolder; + + comp.setLayout(new FillLayout()); + + tabFolder = new TabFolder(comp, SWT.NONE); + + for (int i = 0; i < mPanels.length; i++) { + if (mPanels[i] != null) { + item = new TabItem(tabFolder, SWT.NONE); + item.setText(mPanelNames[i]); + item.setToolTipText(mPanelTips[i]); + item.setControl(mPanels[i].createPanel(tabFolder)); + } + } + + // add the emulator control panel to the folders. + item = new TabItem(tabFolder, SWT.NONE); + item.setText("Emulator Control"); + item.setToolTipText("Emulator Control Panel"); + mEmulatorPanel = new EmulatorControlPanel(mDdmuiLibImageLoader); + item.setControl(mEmulatorPanel.createPanel(tabFolder)); + + // add the event log panel to the folders. + item = new TabItem(tabFolder, SWT.NONE); + item.setText("Event Log"); + item.setToolTipText("Event Log"); + + // create the composite that will hold the toolbar and the event log panel. + Composite eventLogTopComposite = new Composite(tabFolder, SWT.NONE); + item.setControl(eventLogTopComposite); + eventLogTopComposite.setLayout(new GridLayout(1, false)); + + // create the toolbar and the actions + ToolBar toolbar = new ToolBar(eventLogTopComposite, SWT.HORIZONTAL); + toolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + ToolItemAction optionsAction = new ToolItemAction(toolbar, SWT.PUSH); + optionsAction.item.setToolTipText("Opens the options panel"); + optionsAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "edit.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + ToolItemAction clearAction = new ToolItemAction(toolbar, SWT.PUSH); + clearAction.item.setToolTipText("Clears the event log"); + clearAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "clear.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + new ToolItem(toolbar, SWT.SEPARATOR); + + ToolItemAction saveAction = new ToolItemAction(toolbar, SWT.PUSH); + saveAction.item.setToolTipText("Saves the event log"); + saveAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "save.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + ToolItemAction loadAction = new ToolItemAction(toolbar, SWT.PUSH); + loadAction.item.setToolTipText("Loads an event log"); + loadAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "load.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + ToolItemAction importBugAction = new ToolItemAction(toolbar, SWT.PUSH); + importBugAction.item.setToolTipText("Imports a bug report"); + importBugAction.item.setImage(ImageHelper.loadImage(mDdmuiLibImageLoader, comp.getDisplay(), + "importBug.png", //$NON-NLS-1$ + DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); + + // create the event log panel + mEventLogPanel = new EventLogPanel(mDdmuiLibImageLoader); + + // set the external actions + mEventLogPanel.setActions(optionsAction, clearAction, saveAction, loadAction, + importBugAction); + + // create the panel + mEventLogPanel.createPanel(eventLogTopComposite); + } + + private void createFileExplorer() { + if (mExplorer == null) { + mExplorerShell = new Shell(mDisplay); + + // create the ui + mExplorerShell.setLayout(new GridLayout(1, false)); + + // toolbar + action + ToolBar toolBar = new ToolBar(mExplorerShell, SWT.HORIZONTAL); + toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + ToolItemAction pullAction = new ToolItemAction(toolBar, SWT.PUSH); + pullAction.item.setToolTipText("Pull File from Device"); + Image image = mDdmuiLibImageLoader.loadImage("pull.png", mDisplay); //$NON-NLS-1$ + if (image != null) { + pullAction.item.setImage(image); + } else { + // this is for debugging purpose when the icon is missing + pullAction.item.setText("Pull"); //$NON-NLS-1$ + } + + ToolItemAction pushAction = new ToolItemAction(toolBar, SWT.PUSH); + pushAction.item.setToolTipText("Push file onto Device"); + image = mDdmuiLibImageLoader.loadImage("push.png", mDisplay); //$NON-NLS-1$ + if (image != null) { + pushAction.item.setImage(image); + } else { + // this is for debugging purpose when the icon is missing + pushAction.item.setText("Push"); //$NON-NLS-1$ + } + + ToolItemAction deleteAction = new ToolItemAction(toolBar, SWT.PUSH); + deleteAction.item.setToolTipText("Delete"); + image = mDdmuiLibImageLoader.loadImage("delete.png", mDisplay); //$NON-NLS-1$ + if (image != null) { + deleteAction.item.setImage(image); + } else { + // this is for debugging purpose when the icon is missing + deleteAction.item.setText("Delete"); //$NON-NLS-1$ + } + + // device explorer + mExplorer = new DeviceExplorer(); + + mExplorer.setImages(mDdmuiLibImageLoader.loadImage("file.png", mDisplay), //$NON-NLS-1$ + mDdmuiLibImageLoader.loadImage("folder.png", mDisplay), //$NON-NLS-1$ + mDdmuiLibImageLoader.loadImage("android.png", mDisplay), //$NON-NLS-1$ + null); + mExplorer.setActions(pushAction, pullAction, deleteAction); + + pullAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mExplorer.pullSelection(); + } + }); + pullAction.setEnabled(false); + + pushAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mExplorer.pushIntoSelection(); + } + }); + pushAction.setEnabled(false); + + deleteAction.item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mExplorer.deleteSelection(); + } + }); + deleteAction.setEnabled(false); + + Composite parent = new Composite(mExplorerShell, SWT.NONE); + parent.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mExplorer.createPanel(parent); + mExplorer.switchDevice(mCurrentDevice); + + mExplorerShell.addShellListener(new ShellListener() { + public void shellActivated(ShellEvent e) { + // pass + } + + public void shellClosed(ShellEvent e) { + mExplorer = null; + mExplorerShell = null; + } + + public void shellDeactivated(ShellEvent e) { + // pass + } + + public void shellDeiconified(ShellEvent e) { + // pass + } + + public void shellIconified(ShellEvent e) { + // pass + } + }); + + mExplorerShell.pack(); + setExplorerSizeAndPosition(mExplorerShell); + mExplorerShell.open(); + } else { + if (mExplorerShell != null) { + mExplorerShell.forceActive(); + } + } + } + + /** + * Set the status line. TODO: make this a stack, so we can safely have + * multiple things trying to set it all at once. Also specify an expiration? + */ + public void setStatusLine(final String str) { + try { + mDisplay.asyncExec(new Runnable() { + public void run() { + doSetStatusLine(str); + } + }); + } catch (SWTException swte) { + if (!mDisplay.isDisposed()) + throw swte; + } + } + + private void doSetStatusLine(String str) { + if (mStatusLine.isDisposed()) + return; + + if (!mStatusLine.getText().equals(str)) { + mStatusLine.setText(str); + + // try { Thread.sleep(100); } + // catch (InterruptedException ie) {} + } + } + + public void displayError(final String msg) { + try { + mDisplay.syncExec(new Runnable() { + public void run() { + MessageDialog.openError(mDisplay.getActiveShell(), "Error", + msg); + } + }); + } catch (SWTException swte) { + if (!mDisplay.isDisposed()) + throw swte; + } + } + + private void enableButtons() { + if (mCurrentClient != null) { + mTBShowThreadUpdates.setSelection(mCurrentClient.isThreadUpdateEnabled()); + mTBShowThreadUpdates.setEnabled(true); + mTBShowHeapUpdates.setSelection(mCurrentClient.isHeapUpdateEnabled()); + mTBShowHeapUpdates.setEnabled(true); + mTBHalt.setEnabled(true); + mTBCauseGc.setEnabled(true); + + ClientData data = mCurrentClient.getClientData(); + + if (data.hasFeature(ClientData.FEATURE_HPROF)) { + mTBDumpHprof.setEnabled(data.hasPendingHprofDump() == false); + mTBDumpHprof.setToolTipText("Dump HPROF file"); + } else { + mTBDumpHprof.setEnabled(false); + mTBDumpHprof.setToolTipText("Dump HPROF file (not supported by this VM)"); + } + + if (data.hasFeature(ClientData.FEATURE_PROFILING)) { + mTBProfiling.setEnabled(true); + if (data.getMethodProfilingStatus() == MethodProfilingStatus.ON) { + mTBProfiling.setToolTipText("Stop Method Profiling"); + mTBProfiling.setImage(mTracingStopImage); + } else { + mTBProfiling.setToolTipText("Start Method Profiling"); + mTBProfiling.setImage(mTracingStartImage); + } + } else { + mTBProfiling.setEnabled(false); + mTBProfiling.setImage(mTracingStartImage); + mTBProfiling.setToolTipText("Start Method Profiling (not supported by this VM)"); + } + } else { + // list is empty, disable these + mTBShowThreadUpdates.setSelection(false); + mTBShowThreadUpdates.setEnabled(false); + mTBShowHeapUpdates.setSelection(false); + mTBShowHeapUpdates.setEnabled(false); + mTBHalt.setEnabled(false); + mTBCauseGc.setEnabled(false); + + mTBDumpHprof.setEnabled(false); + mTBDumpHprof.setToolTipText("Dump HPROF file"); + + mTBProfiling.setEnabled(false); + mTBProfiling.setImage(mTracingStartImage); + mTBProfiling.setToolTipText("Start Method Profiling"); + } + } + + /** + * Sent when a new {@link IDevice} and {@link Client} are selected. + * @param selectedDevice the selected device. If null, no devices are selected. + * @param selectedClient The selected client. If null, no clients are selected. + * + * @see IUiSelectionListener + */ + public void selectionChanged(IDevice selectedDevice, Client selectedClient) { + if (mCurrentDevice != selectedDevice) { + mCurrentDevice = selectedDevice; + for (TablePanel panel : mPanels) { + if (panel != null) { + panel.deviceSelected(mCurrentDevice); + } + } + + mEmulatorPanel.deviceSelected(mCurrentDevice); + mLogPanel.deviceSelected(mCurrentDevice); + if (mEventLogPanel != null) { + mEventLogPanel.deviceSelected(mCurrentDevice); + } + + if (mExplorer != null) { + mExplorer.switchDevice(mCurrentDevice); + } + } + + if (mCurrentClient != selectedClient) { + AndroidDebugBridge.getBridge().setSelectedClient(selectedClient); + mCurrentClient = selectedClient; + for (TablePanel panel : mPanels) { + if (panel != null) { + panel.clientSelected(mCurrentClient); + } + } + + enableButtons(); + } + } + + public void clientChanged(Client client, int changeMask) { + if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) == + Client.CHANGE_METHOD_PROFILING_STATUS) { + if (mCurrentClient == client) { + mDisplay.asyncExec(new Runnable() { + public void run() { + // force refresh of the button enabled state. + enableButtons(); + } + }); + } + } + } +} diff --git a/ddms/app/src/resources/images/ddms-icon.png b/ddms/app/src/resources/images/ddms-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..167a83b5755d21045642edfb5cb99557e5ffd4fe GIT binary patch literal 23410 zcmeFZ^;?wR8!h|{AtjBVfS`adgn)o_DF_TmN_Pm--K~N&NJ$JODP59Mf`GJicXxN4 z&FA}`|KR=cT$g&`@Z{ciuC?w7Qjn9x#U{swAP5&BC9VWPD0ATN5GD%vq*aYz8T^OZ zOiWAxAtv@j!QRHi%+eTwT$5s>`YebQ3ED-Fa7GoHp(lR^l}g~5wOrY*gi3=WZC|Rn z7-j8coU0IA<1D|BXZZnBb?yV7KgB5pKT${9RLuN@DI`_#{DQ%i5XYa!Sg7C^>?sKb zm8VJkc`wop|C3(lmwrr0cA9&&NRJq&HR|ES{6ocid|kF0rJ-0Fz5hl}stBP{*MpS2Oa`9Yir%6zy|#j7!NI-SBzf`?GikHjp|Cs;r7vbdg>+_&UGK7a5@ zrpc|9-3pTxHN)Q!;aGg-h|Y0H;5r=gjKuW@HQl0qtVWji`oWCnTI-23AvE9ZTk((@ zRg||Bubo<;cLv3ewP`+l@k-YvspSEt_h*+&>B+6D>9{AB&58{I<(n-ZCNsRZcF-X# z)n8vxSsCWv5J{vo{d4&m{Z*bQu)Th9hrsvuBaQCAc4mThe?H$9zof7#ZfBf(KP|WV z@u|L1e^Sb!dhvT3_X3S~d?Go*$tPna#*HW5&l~6V3lDI|N6p@IQsqx9#k5yxm3ym3m_1Mxe29x&o&3d@)6% zc*N4KnVmc_-uz`)WnsdL7lq7ZMho$D&!8LVVSnD)RH5f01U-Qe;;)olleVTUERD=k zW>3XZa2Ov;2ax3dWl~&WU4Py&%)H~Ao2?@)__Fd>o%6AEcCrozR>{YCG-pfR-_1cr zf7m_UV*khyEXgMMlhypTC7_V|)btB;DzLwNd>gH(xmUSQGuBMTSmqZwV)}iYcm<|C1540t>VW z+StIr-&vB0bziJC9qEy7Dc}xS)~p{ta5saXQv!u2-zv^N(IF7DEQkTktIdc}^3gZt zCaQ@h6^bmQn?0Mab2o-*V9=n1=tp(D0~1!OBX8|@pTObu*9@xWeEjwN!u1^avT@%q zp?F~w>_3L8$aAIEytMnqS>G}Vz2n0kbX8n`LMXv}$3;ay+TtCo1j4f3v7`%yn$kUh z;&GD}KH7%M6`O}yOzKgEnBte0;?HXJ(2+jYegHMeqLE<4%5{slXbdnKM5;y?qv#%!urm0- zAzuqjc(r0Prd5H2^0OZuub3l`Ci;+U_%efmjK3Jj@XKFFQU!;iKn-G|uc-4V$2!}} z5MFB^OrqVnbLDBXDVJn(P=duJU{K@Jw6w_!bGkTgF5}RfOtU{E?H?wi6`>|cO!%3m zvRYM{A_b=u%Xa*MVI!|w-9NDdzleV!;L6^wL_g9YqL?u3&5UR(xX#6lqid~=#7%`-5qUOD^lV!2AN$_FgY`lWhO*~N7k0?vr0Ye?Y{CnZcKt@ zglue_)W`l#K29Xk4q@9#iNS({bxxpV&516pPLMMX+g#*C!EUH*Am`)RG|B3}%$LFM zK$>N6#yJN0#RiOzwODSg9odWc@SSNf=jFf930qIhL(6f*J@f3OckI3Hp+f1T9M`2j zz-wW)&L&1g;T>dK^r7!Nd0!6SIf}DxQCvQ5`M{La8;Qn5^XCoKqj zGm@p!2`e_ggOva_#)d!3dl|u*sJu@kR^D7rVz(yUfX0}>6&kFIzViMq4G-oEy^*$9>Rh5&1v&1FS2vmmk& z>aa)J#F+cCc-phpKFiK}WZF{#(;`oHoHjd2O5H6=hB8@qpw4WPd^!5j8_@(~jAJRDY~zvw?bza25C9mrdTbb86NTs0b5!m*vxBwK(}U$5)$1#H)Q=cH<;luvptx=(kK zw<{XK;=MN)9`+&B?*Z%s$8WIE!sW3+7i)B&Lh#aCoH7$Cz5m#!Y+HJFNy0qHV@|L% z^5EdLjpW!(G16E7_EUVLc3lS@DUXtjZn2?Y{drTx2yxfk zr3>)iqS%be(eX7qBohX=s@DnaV$5}&S;p~uX5!?!%Nqlbqh?sy`5Sk-dcXfD3ih$g z>FQrMR&yKg839qaGq%n5n-Ced3%{APt%sNY;yvrcj|V|ki*nBLg)Ekt_hbjDyP~ye z^zulgRW^OX1?g9CVXf2!I1BZX#z-rRvJQDa? z7cSQ?2A-Lsos|{29PJ5S;rhh$M@uw&>-~l~mYLrUvNnA2_TL)XO}*+kF_pG`-e6jh z42l}zNdcq391`>8vu=k7{0?FM%E6JBMgj(Y+TH?METh()Fjku#$AOqR+Hk(=*L!?j ztZS1enE^|MZ;#|HbAS=-xJ-f7P8F_p7@QYQBZjqNT#Z?eIGy`<6*7E-ZI|wV)1}D` zsHZhCZoy?1T^cTG-YtvLm!5&057;eqpG+jiVx|_Xjz&~FhU4F~l1AXuc{&x14GgE9 zj_r^HAf|?ViH24Bn3ZaN+Bv$ndS4x#y%>@I9Q)a~z3f(+303-yVTM}Xm=J#} zJh~9F^&6FxIa|<+XZ_cyi7h4Ujp+bb( zq6Gf~4@bk>Pp&Y*k753suwtcRRHi92N+Ld!MviS5H0M(Hii`{wPV62fGjvzdsM=_g_0;~o6tF4{tW40|sl_Be z0fF88tFO=gATuiyJbExD?InxcQa_7Mwd6lWlUQGrvxi!OpVDE*DMAfCA@ACw7c*bX zS)h@;0yq5Nh4g&EDo2jm(B|Rzs^8bJWoN%Nga1hzym}S)okOlx#x!&V^Yr_ zC+pa$Iul(@qI&kk6;B`fVs}=95-zzV>Uh+o0u+%rSsObY{#E&jXcR)k3;(+b;oUVs zmY5Ov=n~;YsQ|)!eQ&POr771v&oEyanofqr&N!y_4{G()vz={IdZC|z!zcVnAI;w@ zwa=PACW6MqMF|OU;fwAKE+fZ;DtKVm5@dsgldOIeTE4~a&@FB*?q-4+OR&Fp!y{A> z7e_Z{0okj3V(Ti92_t2Rs3E?aJ0=^Ay0qVjj3Mkp+li3%Rf+%_$^1WGj&Jr^{llgp zjU*r#KtE)X(M}~t-WRGaes~%FyzPxBA>MLysi4oXs?F9`7IBE2NriuOTR9za2(jVd z?@L2Tt{~y!=stc<0XOgiw-SEr$?>q!6o!bZEPIitxYB;VA?7lfDx20`xi#D&6BKC$ zcU5Zea^k2ARQF-}OqR1VDZWR|L2pou=3N_UxpJ&k!{KK#N2}$^>8g znaXo2*Lk;lrmQK<+wq3R7fU6PQx?oP)Vm~q;WQa&Om`Qv60s5XaauWM@(f*B;LBwE zok>WQC`X3~zH?7M9GOp6Xe1wP{V=RDi6#FThQbU9xBKfK6svutAXGqWCZtlAVvu{0 zj|)or@5a&mkKU+*!xdhA6z8`i`H;r0%$-|YEK#S6e(@v?(+?Y4ilH|h7d<0z{VgRa zA?(c)%J~F`Oyc(b&z}_89o650bJ@|1Q?@C;Nil@y^Jpz&TfraF5UDx0J~dou+>pS)=9cwZgrOGEG_tvd(&62o7nSnWmmT68PCV84t28{9-P z=jvOcKxVXAhCFFT8txm>I%BfGpv=iT7}xZC8a*VL;D^~rn_TTHtPx6M=J$>n2e^wa z8=4+%nk%h=U%_R5w{*s#nsWU(KU|xNg!TAqQZU^kup>~I`WH@%MVlEA)2kcpbG1(9)%d4?8|;V&!}6&^Rc>~7O2=RjRTR&l z#BN71=<;^Rwv@Omt_Td^vRKx$TJ!f_fr}*ZJPCKqeOVNcef&DN(_%9;fjwEhANa@R zjoAPuL)gFadQRQigzf4GpwyM1^tp1&qgMqdkIsP(>bf7_uZ1 zdB%r<$AD+Kn$Gwb=De73-D~UVUtg9?^?spBLHi~qA(Bm{?cxg5i6D2c41C$vD*`2d z_ueS@V=tXSP2^&%bEe#QXlKZBq5lwN$5=~ zN3{CXTZ(9WvXPiMP|49?dzF<0CCu#IKHn7Q6J z9nHVh^z1L-q7*#XTghvP)ZDm8qr6WSwkOc|K?9$0)x@gcT^bhcA6X%9H4AMYcU$2( zVj_vPYi$~2DDUL+rx>VtL`jE)38SNhGt>xuM|<`XaxJNc-+?(i@^q=wJzw7E4N5u|Qxob(Of5sjH)a8T6 zhvOBe%aI>`g#2uydiOm?J)*yR%pho!y8XLi+qzl?{{4P<#oq1PfDuY8Vai#*_1fc) z$%Jj?Xr~J=E=JDMK-)xTbGh1wQ1V13Y77z~x_oKdnB54lsqh@Gm(R`$U96qF*vVtmYho-7 zJo)`u0OThAe>opEV6E<-O;r=48+qwN6_nMYC8Pl4!hCd))!Eg;!}H)Fk%TEIw)Yz9 zw~t->3nGv)8H7$p@>I)llHZl>w&NaUe;=u?yfOUqu_PRMNQ2D~HqR@~`0l}3tls5L z43#E+N5qI4|7_XTY7f#I7nn+gM7~0vqjhw?{SyN}Bf^&ke)k%WJK~srwHqU%3<*`D z=qw`Ie!IwkM(sa~Zn_|z%q_*Hy7Lsah|(`>Vxq0~+vEA=0GT+=Kuop^_}5IchW>|N zg8HVLUYc=`y%oFDWMVBCb3C(wRBueEW=yET6a(aJ%i`jn7O1WEg$j9j(zXQ-%m`Y+ z;e|qwm1i4YUObO;m!fhJFfk=Gw)QFRzMoIOwM`D1klFF`fytMy{-@RPRCiY|Ou46m zcjScxO_lb<*&LH6+JB;S&PAdEC=l#QI(j4wK#G5CggqKj!4G?)h0XDw z2R`aj+()8h)5mcuUDInceaS^50WVX4mm0Lx&dHeJdAR=6t!Ia4ZH^|mKE(E;oT68H ziXHSB#93R|H`;6>LT)8zYu)3J?c>-V-iuhtL!Hf_KDJDy=%hb}4UiiFna6Kae68Z>x2boqXtw3nCtIH?piT}KibPKl;IC)-hR(kz zO^6=PWT9FODkY&+RB8HB-j-!z8M*?&n|`UPA4MiaApm;>pwMV!m+-x+7oeNTP#Mu{ zsM5PyDj(E*mv(DjYWL*`DluRtK6b}ySoQq8F~=I55XsdnKPxN8i?TzH+WTn)Sx9!- z3aV(X6tZ3>HS$!?lL>xj-lGjvp8XlT_)2y0$+v8rl*UAI-}nLU>Ah1KA-DY2RC-;U zTveP8uD!z7OIvqccMfXzKt7UJNk7oKOMn4g)YH$a--QOxvl%8fPq{~)Dd&6?LMZt! zi&D3fLBb#58Mm=lFHnjV7;5GC_f z(X!-`!qHdIoO73(PL)P#n$PMEo$ou8H#n#tn5$GH)X?9=q5=dD0OiN#LSC}PtrVhh z>f*E+WBLqf>Mcs^0Z#8OPtxBSmL4wtYA0KkZpl~qUR;FTsme`n5|a~%QcX|V4`8v_ zm7~&7G6k@zYctr_CA$6p;b4=q)ss85c|2Nu<7&wCRSDc|3KPPnT6(Fqfq_LyLpYvB29~3LouyU^_ zpniD6EQ{X(OeRY1PYhS{f6Euv|D5>~NUo=6o9A3Kaq@JC#Fj%B6aQK4Jzb{W_c4+8 zvA{;UUd-X)-hS$$KL(k0G~f59hkbtv6|?L4d*R0FmU0JxnaIs?_H>G7r^1E;U_NuH< zf#)TKi-VaFp3mkb4w+T61K54fThel4jLlo=L>UUMwuyHvqv3MDY|2`@WgdP9#prHt z>yHCLlibgBj1$WP@nWDsYvpbg_0mmIV#b-0vqWlGc^-_fm)E}oJ+<07YofYgWXHA?afc1cd{JQcpte+_?mVl4L78dJXphfO?EYZagN+)G9{m$8?=lhO(_Ih=A&^G|MA9J`z zf7g1Zl={=;Cdq=hH^pl6kIE{#Fg&>(C zwzl>C7pBsCggo7KVZJEczuZ{UL8;CQkcjp9|I2TO><|owqXjzSXLE^bVb4{#)Eh@)~S|?*r04xzhBOoGnOw7sZanilO*rz zvq^pP#KvCZi7z7n9{_7ZhhX|<;jeVOwk7@bCIXZ@fp0KD7l?NLX#bCv5FEq5OLK7| zKMRVt+9tPRNe5@#wbD{ApNX_LqCP1b^Q`*r$kRc!Br87x(_a~$^}fwu{9S0-o!{yR zS`fnlF^Brr!N&?GIDn9Tr^vObLrwTT-0F@Q#1f@;XgC~utkB!Te{Be5f8WIeCD6u= zG}IWs+S2B=lD3tiUEPQWaqiaTe4@GAY&&bo5=gAafkHaf*WS_nO+E|MKFwum_P>u0 z5sSjGtQpIGCY&ZalZx@vMd1GkmfE_9O=K%I4P!n-*8mdWSA=+Xvdz98dS-C2HwjWN z)$FsTM7UsE9znJvpl0zBp~7O`%qNT^8>DYW(4-YhsS{#Jp{o5~Ex>#2ub0;M;m0O` zNo&#oE0;|JxD(go*MfbMR|Kxh4e02u#_iQ5;pYBrNVaL7DSyVwKP(8t)K414aJazV7S{Au@95KQ{lpi>7 zI#CP&zrRSH;{lXI#_T>bbYoY8!`9z!ObrCz(XcTYElu}lFGpTXx0*^3{c68no=3hf zx~ zOyB0`?M>Yd=c=bjenq;KN|?~fVcNJ9?2X}#5c{S!9vn3oj-cJLC(KeW+NZit6Vhu; z(fJp)eD;Z2U;j&u2+~fJa2d3&JZefdm9F^rB3o$=UB7-98d2NC;BEgY*(7i*aUH_e z2uCh!R`dhf0>LjN6njx``yd2hjNW>Hn!z=Hsxo>kP_=8i5oXg2pcznRSNNau%<;<8 z6}%?`9E{8;X}L%DCdHqZscQGa?YKXjo*9A$;hn_uTFCaF-nEv`qR-Xx?Rl9eBSJ|5 z7wywW(I-z31Jq|(fXF#KsonRv&A2`N;Jve|S*W^M>!f3Yh$7Lwf1N9G8FITkdxrF) zt}}Ia*!s|9mr{7g-E~%sDce+@9GYV62ALC|t`*yzZ z+GF=>|ETd`)rW)Kc5WlW=dic&utz}e)=TE1@Yd`Up4{j0j<7Xx zl3SnRuN{B@(MST#gabOl)$k0Eq4ds`=pyeIw@R3khAv&i8UVX_)AWe3+Q?>gUReD=BTA}2i}C!@EA zjaL-6$GztUZhzQn&K3?vZ$ox(TN^K^ude#8u6*SIy23ze+U~m$mW@_b_JW73rJDiM z0uN&*Vp`$9>)h`3T@CP1-`?zA_3Y}62BhX$=u|ZvR(W6Mc)K4KUk&&GC^mn%?51(p zT*t+(TQRa{B%(PO=axk0UIe@A&z+6P z#m^`(^iLDI5x+c9X0<3gV4fz=OHWd7)O@ZlZ|C&BHP(^unW?Q->|WRV?DO1|HzpyB zpCM`+8mzgI$L{s2`6^7qC$~jKO#bQMK1IdFT7?$O?bXK-2eH3D1Zt;%-T6d0S7WJ^ zni^`zkj7q$yEl@0buLn~f9bv8;p1Y{wCZ`$SSLMQf4O_TkQzzX!IU&)UXd@IrOg`G zwajBP6C<4!5`r_xB`-sE?B3G}ABd>@sC~ikD6FexcUq7iE!4uBBRJW2VfZf^MEXYc zrc>`yrdU{*Ph_K!TJh#!BlR{_go!yuTf@7$kxbJrU?5oWZn@>gl)!<8aM18 zNQZc(f%o&1J@XTEltNWzcGsz3hwhC%v0kKEOV95zRQkU#n!of;LM%Q9pGSB4e4TNH z;J$1fug*m-WNvR7ua5hQdw)f&k94=q+&Uet-ZrP!SXVN|Zm47#CYt0b^Lu$|!aX(M zo@Ni^M8mbDaHvF>+U-*H=Hl(asJybqWrok(2k9)Z8+CjlA|e6;mj_*=dJQ)R z1?rxMD`6VkY;46$YO&@O2P+48`GMc1E0dFgwXYplAn{LTRU z^My0GkbazAw#@gGv?au6qwjjM9%j6oN_o4*cGZm>Xzf@{rA|%q^gaMT>5(*Cm8lCh zaS`fHMzidqOsuvYuIWQK)9| z;L#_0sK}Duv+;n-avu^tvAJ$;RJ@wD-=Et&y|8JhzB))vwYeVp6%ZgID0nrG@8f#Z zl9A#0-PYUN+eyWYi+r?jb91wJ#^|SJVMv!|hHk!UQC?hJg?W~4z77wvaHL}R%R;}% zNEo0htb47DSf6=5jpLrWsKU7ps9T?Z zr_nIyrL(fK{xR(T<+95eeW!Sk%IotjvQyROMUi9VltMTmvgnf6@{EA)5=jLY_ zjPp2J<3C1aY{$l6#`K#~oHnZ^KU*k%KRTYfQFVRcd9ZGQh!Vd2kn>U0cCnqC4q=Ya zbNPyf0@L{RKm#j4tp7)(bk@%FWl-3$N6@eJSS^ItIOcPfygd4@Uf5NyKE0Id zAs%m~cdtjdh&PD~nh{bskG_=F*4CDmdf%Q}z2{5-?>O}N>9S-j@Bb07vA2S^-1inO|s0wcDa~w z%O|Ef#f=DiHjSIJw{3(kc4p6u=n($YF*g+qvjwX8Jtl|wCS#(ak+H3<{%4(?)5E_y z#GBbe{rzp(AKphpu_;PSacQ3L=p(}d!jyD@a^uaO5x~G88IoWg|5pUCbD0yd+kSXQ zjtcR-p)|=3SW@;hFmlS(!FWWQ2EbCr)HCI>48NIEnxE+!3~6VhEiTj~KI%mYVdd96 z<099K#)Cgmf@zo&vmdn`Xfnl07q%@j&)cf+nEF1Csm=LUYcq8(rt*K;KACS8w6p;A zU&h>nFegx$BOA}88XN|t^Y&@hvB~CQa`U2#_3;QfpbbM&{GL|K_N>Un z9@^KNj+lq34q(2a%`5j0eFb}F;Abiu|432iF@b_O{J5o3^hwE0ue|4N)$O?E%1)MB zE?k01a{aS#%5xQys(Fk zg|^lUQ2Kj)Se3DuG=APtLaJ9%YN27FO(rliDNbw7l};+H(mojE_zpCrqO4YX&IaG^ zUrSi3-Ofplr=IP9Xx!H^GFrd5;IBH5ZoED95mO0xYsD07x4F7MKHR^aRq;~zn`bUZ z*zx_ngA#Gkj}ialh=tF$oL05|xtBY6I)gvQS1PHTjsVP$YA71&d_92qEc7dFqwED5 z$)+FvbqS}*ng&++#1mkhhOUZHnWkJ9k||g7$HyKSod36S;xA6*du!Bv;m4&AtA*mC55X~-P-!l5=bOZ{l;Y|DIV!A~uDx=%OR3n(}k zuo&*S5Ry&gJ&O<%P##B>4oMffIV z8fPM)PO>)RTDj=6rq3q-@=F8MC1lzCyRum$sbFo@!Wqq6xE~>TU4(=62qxU&^;icF zu;bR>=l$cubE;m0o4&0Q_?}F0+(0@i;fcN$MsIy_vD(NPnS^{I|NE{L=YqrRCqxjl$Qt7XT)-G&ZU~p2!&N0@6f4JIRxdI9rdnL_C_lr#I11HNe+W zSPY~ZoK~)v-&F1xBl`~7pv-6{4A+2dy?PgFee30cwx7lG@e1|h?l&8t1u^&Vdh_nn zw)Vn&uUuSmixN{qlMIa~18Kj#S;lDtRR~qoY~M5Sv$h?Ftbb6YO^+|8;l_r#bYK&;6Jim3svSzW$LLcLp>$qhfqR=bT0Ur9;=mL1GU<+@aQC|^ziJ}@6GGF zj>+HW?^VeG+zPO_IP9#A{#*>PKOZ}+jE&saHV=u9O)S=U%K5fj{FpE(*G*#@uS0yC z|H#YE>S~86Vw}`3zj^89QL2!qx-qKb<7hX`EBtFhfDtI^ro+O#+$gT{)r-99-c@^E z!u6EZe0JuiQ>%#q#G`)dz5aCkqKKe|?GJG(8MBBT4-LkpxR2<2d-kvFuWuJ{$`5H# zSDHG1HyxzgpIx-Q?z%8v(%PL$8L?{PdE|Z%&71H)84c%LQH4XfwQ2&B0Na0xIYYG- z1%R^EKaj;Tmj)`l%#`0dBLx=a11yOzSGwB3;rjgV7*^&9_Qo04UoVl^VnljH>+{;)%u=SW{ z>~h5@ns9l&+?@DIhlryK3bhBZ&(XcZYHu&2cfV|ewQp{BDPUav1498-6m?-S?@Si? ze|%10D*5m*Lk*hoZGtk(=n0NVy`9yA!}DUn{DQ(HB)93&*CfQoUDj=Y)zxZ?ewQhyF8}7}Jel2rH?#@%1CTsZ(FKvP@(ieZJmfyx49Hx};lE zX$8D{!Xm{|QNT2IuTqwOr#7Z_>>F=SQ%BHe4D!zi=nl*ZFb)>rooRCCx==Cn(vhY% zAOV#A)>Wn^8!dH-$O}KPCw8$s-!m6zW*`9$#PkIr)iO&29ioFVZnFJHnz}HX>Od7) zt^vx;gAh}9sh~GQKvj1=X65zDVI_*5?_o6s&h1WI+~}d#AJF5NMDfQ;orP20udB(^ zN`BU3(Q&9bH^$eJNXiqI1=-4S=dKQXS={9UTWYazKH?1+Uckx$VsWgr^g@NFV}D$w zaDG30dyGv!H`X6Z<~L-b&Yf#xoDR(1$b-R{+kH;5G;jnoxkq*TKf}lX9bjqWDB^n# z*Z@tvFu;aI2+ax{Ug5&^#eM}YD4F)>DlUlVxyo1h<+Ulh z2cePNKr*rujmn-5db5kUUcoq}v9*uXKwo_6?qCxF&$vgTDG!Cpy!+&!E4^PQg*Wz9 z*4xRuof?)^uMIuy0>|?nVu~pF40X+Kv-Sbp3BN;|9oEF1A9HXt{oXN2W~}|<{flIz zlcw>!1%{i$8G~%Q6j^hHmI>4ZXq?Nr-^n02Ud{~Zx=YDD{O(7n8^UHPT{2S58p9-+ zqXz!)=scyXex2J)A$T_0YXFE_!FAEcjA`$Q9aAA&30A8D!1Q`JjWl4ZL}mIBYHDe$ zc|8W=T|h5ipxfw}jk^Wpu`S7{pkQC+&sYk1IIVQ3{BA)Fri$XLLp_r!)%LKnr5YX% zNXK)sicf&z;{C|xQCa-`hO^avI}7klVE{4eyK;O+u7dw&5U3gof~xnl7U7F7yt;>4VslG*FY$~J&o>Q0Q z*F~}*UD};mo+?)!-4_!e=u8-b`oS1m~(THQ-U6^@mmsJ~=lJjyqZEE=DzV5DK>MBha)I$=VExaTGGd z3ngS?s2vb0K;6Ql!SIT<3ITbv>a^r7qgsp>959z!z3et;+P3@s})6a@3Cy@Sj zL`iyZ}-q#&z;!MlPguo8I% z_+ZbQjc$J8pQ%8jz*!o&GV!COvg%$15IOPM-`B6K;UjnxmFZr7-0`8|d zjg-oMd@2{`4ltZSSMb(Kv1tqxM>vav7qcdos*hN3{({lpm#N~gW@f8jbm0lK2-LZugIvhkD}fm zZs1UeMFs9Z5i?oES9?!?1~?oYLMK`)r>B5hk20x|mp5X=ocnvu0?vrW!W{*>WVrVy z282$)v1&1+&qT4mj@X?|bmot=#)h}rdrg3)UD%_`Q19(=7s#ax{r75SD`{XPPQ!|{ z>!rmG?;N=-p0#m$5$?+!>i_AL?qz(0y7BQjgO{fYQjr-Ad@M0}Xe8C$%a^3qEd6zY zEHyQF&|Z?-HJqC=S;ws&O}`ey}6K~F#98?5+;T&W!ZHPmj1giKce!#{Hcj-zQNL zW1G)HH{yLijlUGUbFv3rFUh{4K%f4J-N4h;X^k&$2wQE=rlrb{z?P<1Ye0u$r+t*$ z)6pTlp-M$uAn@EyiU=({q`$HrqHbO-?hQ5x+suW!Kl>3foh`M^eQsWUHLny!r-Ju~ zM$=>5Vjo?86#cDv=p^Qy1)P5#?Pf(|KfJR4n)9xHeB-_f)~(|J&M-Y0O7A0OTGp? zh#(*vO@(jnm}yJq;ZvY@d>c&u1Ue!NIjaav?}Sl@*~6>>E$SssS zl79j)+*lANF6_kr{db7Lh5S^y|7l2q=zqt0d}UZjeNo*6aid}{PC(t5vnhZoY^P}7}!3JF61tt zVM_}{X#`WZqCUdY3z6`2efqIo6y|ScVleiuE7u&mNr230Z(-||Q!BvJ0eR{zQha$( zvIKO%ms=Pyf*{}*9PYA$(h!hY0B|ctFu@;SjKUxkCF4dpvC_IRkyF5M%bxh0Z1j+h zQ#M(KoIB6eFQ08O^&t^wgW@)Lrr=hSh7I+D)mEp~Tj0V1^w@AmeOSCzgYaLYT%Jb& z22F&g#ShcBib0$#r2Q!k$fI>BjgSsq!7H{*DK#*lKt}K@;p%8jjXn3L+;_S} z8E7k>D-FyvXd1)1xn;4uHJcSU8g*3Z?9~a+`fY{`gftjFxVLwbN=1;#n5NOm4j0rIACdXs|B(gR} zb|YNS@;?A0TBH+5eByS!AqUVsfuagD%k{caJ^=CqGIv1Yb8dQEw)Tb={a;;U_S>H_ zi{1CrKYv>Py9oJG0|7r6Km{;h>l7+eaAFdBG?=luLq{*m7XiW%ZOO(@tWZ5c0Rhn> z(3CW!gyWCxu~UScARjs^R3~M6&%YJT< z&zryUFu`qKv7|2#NQ`1ItW&xir1$ES;!nVQyP_n{`Y(b<^C9i1b(f~G6HgKbR1o`( z4}>5`q9tx>-K2RGFb1|`nfL%%9-4d~4kk^P_-yV&dO$izNNxgzq#!VL`&*WIEQD`! zljNXE&qpP6LjS;?*6DtF4;lhW|)%`q)@{g zE}izj;UL3HVXIZt&Q~+m#WSGL+gy^XA1~X#n}d(% zm&L;7z|9J?)_B|D{rwl#@Jwt1_}un$_S^mAk$aPi<-WgbPh2un#Dr)Qckn~&tr(!D zpU*-|z-+|*jp^A)>*{6KAB-3zT4BGS-lL%~2zq#O_q_nVW?CUGv-~)3i(Gsdz!a;R zaFVqs<3B%i=rz%XKB0-k?oD|8L6A5)IK~j36`_z6J&{o6UIGTTOUdLr)joh4EEY5*pz+28r45@im4oU;Fvvw=Mk|F6Zx?cYo&Q1Ry#EAgKKX41Gs|fkKXV z+Lyz?4Fj6v6|(%W!^>WDcp#2poQnZq&y>nz=++poGr*qI8EpbDvqiW&Pe}?Rw?tOpWx;|Y7Qbf8I@U01h zEts-z#dgP!XS7h;ISPGWpJG9QX3eJ+O>6YyQ%TaI{bKgt23w&gnOL)E7%p~E+PRdh+;y`RP8%BULi441p%X|IGt`N4`nBm!4G=Dp zeF!s-%4LDr^SPe=0P{;~PpvX(blG5>rsRu3;|nk-Zmm)i{o}bf_(q2~y3ntqGV!LY z=yUXUk0b8qRu&yfg<;JRX6JKf=|NN{4H(8zlg1yBM?(67PghZ~kHw%h8NWr&)g&Wd zd4*Rn=v3TkH|M+UIbPDsvplp(0p#6@5d>Cn62-Y6$sffOz<}`y)i>}p5<4AParL?= zR&d+@X0N^!(1A6o@R}tCUoP}fys!v$o{ znH)OAR#VxZ5wN5^VLKnhJ_(S?Jj#bxd;o#z$Y2KxB)HNm(MP|IhWDt}i}U;@{O$$% zYD(@-VVP#I{rhj5LQ<|={#hD(HUZQY#nlYPZH}l?53ne?szrfp9&zt)B_Fi{(Bg#` z+=o_zX;tuP(-rUv5cszU#HUc828&@BQ{n(;)%U@G=$G^f z{W)XfnPjp4PcK&61Y!ufFt4!cyz2q~T1G>TDh+=DEjMqbxsrU(Q?)!@U_!$k23yNHb;>Sc!5J8w! zVld^deZ_z{r~dJU>F;aw5|iF1sItn#-CxzlM~U>vPfs6e7x6VBjHLAmTEGODHr%Q+ z&>h@ZW=mj=0b;O07ajTA^11XtYFs72F$tTm}QgjPhEw|V9ZvLk9wY9FgO+$m^G?FT0 z8wGG?^SQ6z=Vk`;nzHv&As35IEog=ZKf9jr;g-e)9WLZ*$c=&~SCoIqdlIz4(6 z9e0HSxgc%GD!Fv@I7Jz~sxpCdSK5n;8PFmPm-V(5qh(VWeqLbuRgSX%y`lRGh5N`Z zd_11+mgI2g(}*bbGf_iL5AhF&X3M@KS{4w`_)4=w>B3`NKE9m?e|^j>E)WxZ8^2Pn~BP5 zRIZdxpDV?|uL+?)Ot(X4&cNx79spDs&S9((WmMb*Mb1R48OcrHcwfj0v)t$67 zjLI+ey?C=aaC8v&M0B&@D+NL|AHtKZ^U&&wVTrpM0@)b8XdV#|NS*c|`RS zeY8K zAdH?T;j$0r^o=r0JRi?VZhefz--J_-a(T%9%<>xk+W~ zH>r-*3QDV`xHUk1^f>a(#mV!WaUo-}`QevWwUQORqMHi&iA_%LV@nl9i zR)*%;<&xZu+E>zTRi;iX6u{Ey>40h(n(r@`zpYp|Y{ z7?5ab^ABY)b)+l#Y-5?f-@}YFeS1RJc~X=NMi}Y~7s6pl?{|kDljn*JvwjlvUW4|f zvGoMQuPn&c0sB>a76e-4b73Zg6xyNBdylT@v*)%E?M;RBLB3{w=Q6Awn?WHhA_zhz z-O0wZHv6#YX=*fEw*F)HH=P%lI)MQVKX#M=w{6QtqxJh%e$t}8>+!%NY`_STjrsXL z{8wv~kZ-e*N=(q%0GDLxnrwxgM^~QIwMz;kH64j@T8sx*=7Y;yo@p-^HAC8gU(R-5cS3kylX0a06BnGjMP_)Ju@ zAPYgH%9b~3Y`sP$Bbh(Xgo_1Hv`6a2s)!~Dxh4mx#Fvnn=sjrq1Gv|ah`a$n^2C?R z8*XAwMK6pVTwU7b_4nP$@5bbuwaE=RfJ{KvGcfdv#<=6xLWpsR75M71U>`_Bq$U=9 z>pO=q3##jUNa(mw9K6m;*q-P~thy*rgyfU+x@P?=;v{wcV5_DSq%Pm;~4BW7&4zxg5rdH!wb`*3AuYrc%25z1!a@31744T z+lj2A#_D%eqVvm)b)Eg{KYX#KQpS$Ca>4o#0#YO9VgdIcrFg0JG?$vM4PhiyHx4{^ zN8x?TcN1*9*$zCJ#zE8qQa0jGV38)jEgA#^)L6f_?kQa|o8tH1-nm1*N;`H*xzpW$ zZaI;qNvOlUlH|jq-XaKibdz3%z5{*RWTQO;EN#6DDG_4LnhJnV|Uz#;^$@F;1gN^f09T2^d@UBE6Kr^N+knSABoIu;==_fYoIS9_Q zvL)%B?bx@$TBoG~XfvfgbWlxRrX38zHBwd{k#k^R=ATRpV0F1ccj$58(f-OHL_M-AcrJF0*-@4Nx+YjXh@K1 zNt`KGH+Z==Cs%-NuEW35mw$&xT)Rh&gpfg%Z^vMJw|j2$$n5%1#5J-|gxcQxM7zyC zBhbiWBS?^j(OrK_rucP;OuipkiS3WqcwCF*102u+0EnvAQFep>$X(NX)D{~CDNiz{9;5;E&4$v z-1OuN(+7c3s1TtJX+j{(HNktn6&YH{f-u_T{zFHeX<*km?5My;4Si^#@$jOxuP#_| zYCkCD&2)4XsR?9|{Fr|~l5rc{@lvOirFE$Qx}QBp9wLD8i|)ZIU3tM`7Q3u8Z$xJc ze$69@$)8zzNBe$)caoj$*3ie$s#Vj{YZ6AKj9ARSVczEo_TY;!qxePt!$0YdLR$Xj zcKg)ZY`_{$S$kVHN6H;Xp9R*q=rJqaMt@L`$^aM`Mvzo;iRM;>tU>bzhZ7#9FZhPQ z8#o<9zL5?$DCkP+{PSp>>7frm=7G#)@ZPpKK%=7zt!3*rp(%`3P(1t60jz2}-8j&L`{aRZ3{TN`ZNL8hs5<_Dk!r3%Xbp8lO_7D2a`wx$wAph^KP&C39 z@F@B@*sXCL?BQ32+A#2dopG|exf6Eb9nP@FyE}hp^O}S zAPO=37*e`^rS!a{ut21}LODTdEAp-V=^BS%ECRfE451 ziep8(Rcswffal4Crm6r7o{flzbz0@g%!Yp-+Ay(7V=AVWIR=-|AN59*wQCk5VOD#pudJMkZXQ_x6hx3{`9Sk=|v5F~0%QNq)nzsK6&^tZk< zfua(0MBn58N7o@w$V!sEuwvlm{~wDbLkD)&Qz5uOp$FNX-yA5ACY7X4$FNWj6`~A+ z&Z{! zw7nndavh*VI%?cXxwYo$H_2~U&R5ni7;DTZLU5@=?v{5>Zb$5%4BOhUoJ?P*o^!7_ zh$vcBI!dz8N$#i&>OQbgMVef%o+`>4&YTdH_ewIK{=xD6_maZpE5RtlO=qXuA&Wt) zATW*Pp&Q})Z1(8F27Khk-gal|BnrV_EVa}aQ5~c)C^c8@reC4oY}{Jeu3wv*FrId@FC^2v%YC4*b**rH{7Pde0n)F5lkutKoyqK&;l>Q2XQhjGD zZ?JWHUe;;9CN&gAjo`hPWi#a(a7yU!7uVVhNPHe)91{7k6^&yxs{rQJ3r(+O-3_NO zr#Fr3&1VP#h!js>y(-6R+b-<;t>;zw@BNAcUJ?&idIl1`3SN(m@eUM4t;>iuv8;8; zJ@i)pWdXbmU39 zGB7srshAx{;*xmc@*|nWlwWzteoSxYV~|P&<{qkspf=?jJ*h> zK~-+XpD{y?aP~p&Vd`hbJKeaxF^-HHA@LlIxcIUh7?ra*h_&64T)!!Be|mFS9-3cR z>9rFpzlccc)X}S*+&t4g4OJ4TNugsgrF)#WlE_A6D!#6317Z`$T=jt1f2A@|NedH zM@l&O?}Ugw=f--%uZD6Om`kVMbIY@W2WZp(^*$ho=%r0|2O)7vAODAMXo|dECxU5~ zgX_}nGLoNV4xTd~Xk`3qXq zBVXJEY-#%DGX9>*AdxKHQHS|L5G4^!Vvg(rLZAAbo8Bl#E9rw0m=up5CiadV6(#8h`hI!J%B+?w}*Gh;RNE0yfV|^r(}M z=Q1~^D3>)fuiJ-+!H9-77+N+KVz&@rsaJ5zW~qjll}kz literal 0 HcmV?d00001 diff --git a/ddms/app/src/resources/images/ddms-logo.png b/ddms/app/src/resources/images/ddms-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b4708b4aff47982aff111350a16fba206f10cd59 GIT binary patch literal 12285 zcmd6t&ktFBY4m9QSYE7P0V~mbfSFqtjyJ z_-x~NVYL@5$31!3Iwh}n4+IgpmLV#H6SDhPqJK#m5T^Hd^$Wcahme=O4SPz2(910Y zthgvu`|p^V{>{l1jL?DF5>1QZqrj-$lMSA=2-{Z7L3Y#3YE3L(l3x`+vsl1$p(7Fd z5QqbMV!)SDAHAS9j55mvlc#2`PjVlTxHXtxRoY!Hs5>6uAD?+7f_CoG+o`fJr9?WP zz~cQo!^ohW=nA);D|tD_hc>wAtH1ZZOu8)uUotDTyCfZs{kuNWzv7GO>;w^p@8b`m z|J3pM@@~Q{4~B<7NpK)r>q<;Z~cc=?CQ~=bMC}H=81JVW5yGfkjsj3 zneW>Ow!i`Fk(?hF?&Y{jd5&Ms?df#gI3!kQo$Uj23`92TZO6uLJLZgbFYBmA0t1_K z_P5h-mZqd66E8C4`AD^F`abf?(sTr4x*Q9tilh5nDmmPVcGn)@hR96^t!a;teHmTw z8q*>kYg;BxCDBd7UpIJ@5rF5qFD6p_4Va8!Zxxy&LqGkt-Ga)mUeYca{5n!~47jJ$3w}fA19A@Jzq6uPT;ycSH`1;~s;?VgTxOaZsdg$m z1^$+5XYESDE8eq-Wo2_8naL+acclN3$+*f+RVz^9{GOmy@2OqB9W9ecA3_Eq1NmClN{Nt{B&t7QuDN^x0{h%gxQv|&> zs55aBGFtOUMVv1AtiW#|cSKXJs?Eh+QCSZ8HgQlz?3N(wxym<WX6aCuqme8I%YTJu_|B?kR8WN315~|@92IpGZSvR5Gy)4!23(j;*Cw`u1=28#v*ZM-+RCrqgNp&oB9XleB{RWi31|qT89;JnMx3?%@d(2hYUj(S2Gi=TRLEn+$D^}`K(oMd0<%s zImv48k39>ImAhrtAtBgQCya?%=$h&*6e)%;cm6iN9Ueqg3qn1stO=6hG)E@a)T^_( z=Ma8~X3DmaA9t{l-kWFt>XcYa2t6=av_VCRG3MynpaMm0{7j6lFLAA^rI5OvLr+Nv zBEq5iY(8AFxEgCoFmJMWe{S4#b># zMbg~Q;%=NQTDyLdn-7kqC|V3Ruzy2Ze59%O2Qu1i z`<8q(Xb<+_uaFX_8HUU;xLmqad56>ODPB#&Jk4XruWslbNsA6EU=U(4{jD3XZt(mI zdTDEiPuFuBy7f|LduB7nA~N&AS)o99c-aS13b$JH+C!ws09eZ?FKXEM=afTcK~%?^ zwSLCcBdHUo0K%AvU&e;%6EJ&@x~-Exvxs_ke%34t)mLxol(t&5^V)_M3O~r-O!1Tt z3eLn(lSz?Gu9ad`*sZ6jnyK409nfTd!^S0yIeX6*Ak!X%6CiuPzG@Vij>|ak%)J2~ z9$!CVl2fUsXGN+I80Q@GOR-+@N2?YKVfteV2d?B`N{MTL9m62yPKHr1rl*omwu5%H zdEA_LT92X`o^YgH&XB9@OJVd==PJZNj9jL@t(UuH)T#G+m}w!aK!&NdBVUn~J-@m$ z)cAULcAnQvnAZs|L&%h65p&t*nVoiXWi!RI3k1wp57RLrriBCLf}Jn;A_E_G|L}7N zv@k$qFf=M6unp_)$Nyp4DV*xnr&+<7q~TOH20_%s=07I+IMq}~EY!9WR0uxuY~V#O zK@d+`?=GdXwDd*OvQvjLv8bq@7DX_X)O?d5jKOsCg)bNz{!$DlNpwwf^K-0{TM9l( zoUjoI#O^Tz-p}m2P{EyKN>v)|bnCz2eV)pKsM=^+ozamG{X!2t-`;wsSxx_sE|22s z`$jHfa=QB|$LGwy9H4(#s8UH4@)6cAJ%TG;-fhV>9gVj~rF6jV}(k z;K4}r#jhcq8G=%f^0|jx9if`p3*cSG!dNj3q$ZtRm|RIse*Chm@mqPE*H>c|(aBwk zX|Lvnbq={p86do(x3>(?b5tv@=UYd<%lIEeU?7F;W{3%hm1Tt`R{FHgD7tfvwOyB( zGC&Z4wc_H?Whug}x+!(nFM0@Pi0wj5jD(Sszawh%T2w^}z8faBd{tNa44rkx$d*uW z+tSLSBZM9jdfm*b(feD#>8`@v$l2R2EnTixDbVSKN3PAoiX*V+(i$eV-G1-Vi&aD2@J3Uq!uUV#NpU<-ka}-lU5jubZS+Qv^|DK6AievLS!|mk2Bk znV~B_R@PY=(#!7Z`3j6f05?Mhc0lvVC@4xZk>YKarM1k>TyQ^>c;N1J;9tn7LK~_U zk`bZ!zDL<4-XkV+w}Tj5;B+<08dJxY5R}cg1zskO zDGX+@d{jQ1KYi&`K5%{%_HS#S94RvM8oPvx#{v(IVIac%o!+zG9#2E3lmbXD zZ{>mKQF5yiLOInZim%kB@#54mGTi)4F58 zNp4I_)m`^E{87m`m}_Fhp~uas2;@b3ZZ1kO)1)HofmMGFjhj+?VX8Jy@mv>s>P_{( z_T4A^B{irHczE#cdfF5Z)wyIGOd8#iEkNSE^`EnsX)>21v2!7Od&DYJ8|$AaL$B8z zj#`-wY#UX!DISEj!>+V{I^lU<)p~VLNfEv`M_;tZ19JC7j9B266op4L2Ikp3k+KAoR;)W^0@7LWOQ%Q?mxQKdq(9R`2akUyJmqa&C z`wh~+m4TXQ@GsP?ITd6_Uim#Z6fTCPlD#Px?E_+@L8kbNSrX{BnTB~n z&o??+1C7C3KbW>Ip8nwMhKJFAaGY@C^Vk#F*~-^l73{vRhP~>@&Hj0Dt28zT2szEN zT($9vLpO}BceAvE_v@!C%cFr5Guo}l3CeDd18lyUZudWH-^OCJP2X5PUIknxc*|NM zCboW_v|IE6$Rf zzGx6D6i5~*L=vNAG&s&BZ&;z}NJOO&n8lH3Z>zigut#CYN0a0Alg>9{#z9}pW?ZYj z{fjddmx63un&p`r3$$u6h(67a_r79YrTZ>&^NaB3YSbj4L|L&EFuINbVU+68`t*`% zEo8*8y2Z)G!W;fga~1gT0&lA0j9A6-yop{*$II~k^BXUM>t-)HgA}3KM=YH0a>>8^ z+*WofT6*S8KYau{zm0z-@7?7Qvr99e3)UB}AEm|#=sh{-rg(ZA+NBy$?T)!7l!YXU zy8(2rD8U`oYAola-9^%cV3Yr#zZG5gU13tPIl6AnSE7i9$p!{^cZIq|J&do^n#Y>h zz4vgJmnTA7mY7V+vZyM@U2=bBfrdR~WzbD`tk!R@DdhGmpX;9A60U>_`voOk_}{Y*~(AwNHQ#~s*X2!^KH5soMhEE*W9pQw*xhDf$2|h2IYt=&o%&?zgYE~z z$M=z(7Y~>Ak86UJo$BjuKxMVQ%PHeETxj(wQqhGfSuR&^jT*ew?kO%Yt}Ju&I-2`# zKi+et?y@Q`*5A@xNTN!QdaO(xL&!;(YDp56vroXEm`6t@wXE-4&8}C($USaV8N;)2 z)vJ@lkWbgHkxz_+MTiZ^l-Hs&C-SJ5*k}r+``+36VG@b93tM)5V=7mc(icZh39vNB zW7XD}4vvL$hngfT(wz|RvKY@W&uFBz>0`wuQdwv?!x|)@t=+_FMGy+7Q8aTTUlTLU z-L2=Lg`FF3q_Cowx=fUNUE&~*YG+lgSWEkOA!?Or0w%bLML?$=Ud|*yrX)>RsLjrB zudFq^>9)CH{M(wKNC27mhMq7pulxPGCf%CR!AUKWMjHI8@ABs$=`3kD`jh)p$-A(} zvw_DV-6{sipMgM$?jPHy43Lwd8k3#2?INA6;NZQ-psi>Vhvvn7-FD_?A}~tn&uf#1 zsnwfd$;N#oZU+Qq6Yz72Q-$b)?ul(6JgGA%`}@DEjn$=__{ZNrs_)EBNV>0G^A4Pb ztxb&Y{JFU$?mJ>TPV+7=A8tR;8TtoJQOxhOX#aH1!omZ>bn3U&C9Z|}oTlJ$MaK_U zz}}2h5Ze__BU3Ho?l=jT1crRQcHVpE{R(mOy|+19@@{15=fj#w$JL+hh!tOP;YYiR zDL=QMJ5QRwL=Ft8EDkGwrX`_3#M*(9nJbc%osYwYdKzw*M)Gd&gYRPxA8wb2zf3ZH zyAE6Me%Qb5KCDyKI_u(d)cX3q#;j=u<&k!^gC2G zE_m*9VGs2eOPgrFx0ExIP+M!wBJ)p?D=N3FE4EP(IO*-WDZXC^OM3*i61!KM;>=q?;mc1kEeHKJKvjjGTyKK2s$C?K3>$k;6>8!>sBn=u_?b! z%&KD=K3RWcDq|Q)%I7nz(rsTZpR@G*_?{l3W)_#1hR-J;I7r@C}=!FQ4pH{TEMpGKJN-c_)vm=zdPl*^p|yQ>TsPy2B- zeX88*r(5>3T&rN^Q&H2DUGri)oy7jOO7brud@>h`!--sk*Nf7f`;G6a_mKR$WRCb# zNRLj})ed)cRn5Z@chJ;jh2+i1k{j>w=8uOzzG~W^wvrV+g2wYsO5)jz@^9!&f;`6a zbbqj)XVORF7tJ~}wqq#(*~P!D?}v~m#mpIU5~XzRPbWsTJ=q?iXIlOx=&U*TlJ5RY z^l~rdVAEa6hW4(eVD+xb#F=e>{lj(=Q`EVinvmr7i1p@lz4A_nmc;hRH(?X;nwj_R zUp3R1HGPx5A5hVheOSt9S1fI9OwGZyll|U6_HL<8CA%xjE=($(^rPbd`=}becex16-CdL>*qhESiRQF zoQ=4f#(JfaQK$6X2!ErCB&%F(bR46R0i{St=4fMnkOng$q<7z-WZ*rk=sx!L51T}p z&;|calW_l@cepLVF3GN|OfXXPM}JdWXuBWLIsb`F-nP+TpY^fvlkV$O=C2HMdr31I zpRC$vS8k8KDQRpyFoi$!&OUS-2Omks9N?X;lQyLXM*!8b3( z{Wbir)`!2Ja?{cVouQ<=v^#=tj!^oo+7-D&Nx$sa>_A$Fl|cHYWd5p~+!w*A#3gUr zY>1rMrJ(^1*$`@y2QQA(d>grNWU38q?^;p8k_~H|S@>b8=eF8jvwTv&NI*^w;}dxN zt66qc%ancExa(W+Vz91|FYCWwT9imx-M+k?U49J8qw9y`^}1$D@hlxG1WK5wwN1z@ z8sQf%>I$BupIfXfi3O=AyFWpR0NZbW!-k$XyQV5!K6+e$+QnH?H z!qFO^V!LV@FSIlB~gGE?Wrl zDnNp?UxO5e&crWB7w3i>jr}5bBlU#>H`mx#B3`-Qp&@kgvx&(~uhi;uvlS3nBQ0}U zKb~fIi1BJO2t0^Yh+UM^xWrf3Q(Jr#^F1U&(q9H^pIqN%So1=V*|Xg5iSzy&ehmF{ zzuZpJXy#n~w(LKDh6ZI~GOf{3gmEO#qsrbX-jlalXH~S$5kc_cDy?M2cu8XbJtOEH zJ&}!8iY{k*J8gLB{fY?O+UAZk^1woXG{&Mx+=_`&ZnTx|K~LFk4WMN#KsJ zCMmEN2XTPe&ifIc4_4w$4DTN)+qBke@kx3|0Sq+iT~PO`4^%5-MI1K_HqsAua}&+j z|4794!oqz-Xi=MbY(C(&^Wx9!iGq%jP43qC)DH(a>KA?_~4BbHDk7;*r}^$psa>K4>kQ{=x4R5%y>@IPKcX;>Kd^K!^1)FMP`gy z$Z}9epC4GLyE3T61!e#HPC|3;ScMP^P}GlMuq+C^by8|)Cc^i-TCHg9GmbLC3BK|o zB3N^rF9A;cX2}-RY3Jmd88S0VLGvXya$LU;prk9m=SvA%f&+3Rf3#lLp4)y>0~Wbd zG7cUN01IF){}V&xK3~1b(0BAdm7qT#01HJF4wh&-AcF-eTt%`h#2)>nFf^nC49fM0 z3iTlfi9F}?VfVwwBZZN&qD+lmIOnLGE{SU<_PU(uOl3l`Q3~eR-?4kfGHOuW%Cjs{ z<}8a+P*XC89{3svp2`C<@1h2owmiIqD6SAi?0WrHEGv(svypf60G@$8oAXUOC64M7 z37h;!vz!uiFfY0qm^L)pXi)XODJ@wXG?f9&6T6gMB; zh8>(e=JUH3Y!05CAWP5gW8dwIAN5&jzvAeauRK~(tS8SM zf5EYz6R`QWA+F$oaTOXo$_%_V(tmr2miqcf$26}34GHwq?esfdsAme9Hki$%62Gm! zwNrjN-(DcJK%z$1XIEw5xwOL;uFUH1HsLvzwUd4Wb_D9Pr%aPB3@G68yByER!&54z zvfAa(E9|5I2-W_erP}r31NTKR$6gcV@m+lPX%Q>@aWr?bkk;0+T4a*enJ!!ANhCG{ z{BbV%$#A^#a9|v+N;nNDT;5KDo-aZbyBXcud!~5d<249u^ z7Jt1tn%e(LqivjTD-7}G4}1z4dsCvZg;w^D8y!X|mPI-n@}8bLhEkfV4XJq>c3qdK zfeBaJ@YwBW{_fSE{OE7ZhB(r;J<9_|H)2u?D00nh*7<~nCr|$` z#{j6XN6b`r$wI+MQeaB44%k=2mKkai!%znDTS^w7GSGX6VmzB*j-RPD(O5(KzGr|v z+TGc-_AleB{|D;?!}M&`0QH=**t56f;DVK|{v06|7p}3mN6sFVH<>L!>DDUn7P0iT znjObObAYau=(hO{Kxtp)EE}IX?DIph01T&FP-Dd{%x_r%;ib70VgPS#r%N5Gs^wvz z6iWuVh_GeQBqoT=2eb4j^wue+hO|L%W2YDGC(k$TF>%eMsBSB-pGTctp6DwR>BeVR)l(IF`@v#LO=b5L*?DfYk? zlv5eVH(}4JXT`R(X4aNr>H!?KEghNLSS7hIEY^Dh(g@QHC4gV4SG%3==7+}*P4#__ zH3x6K8hGxQQ*D%T@oL<&iH=fG^xim|kz*gokj>Qtu^rAC|8l)=4`5Z55?K}^_VkUU zpwY>Wa?U}3-l*TZ%+^{CosBKFv*d(J&cCP@SmZn5VrdTJ+tA%H(ps(5=;nS_0Ga9b z@^!N3&y3Q%904TjxWr4z)z`=PVj)G40cve=cQ!t&V zpj%alK$J6buLwW4T`vGAdDV$-#e&n@cv)_a8n@mM?>jSQEG+N(kRoi)wILp~Fz2`C z_wbYcg}uYt;89LT*^G^A z1&Xl{w%h8fhKCa@H=%zoIUo~!&@y}(L?pNQ)FTQ_T`{K1%#g@J$aW5sGaU9Tu;xRZ z18-5&dBg4`2>zUe&PO9HPW;z%QD31g1%$1QwWSqSoB-BEfzoInujGvJqSSI1_s3y? z4#q64{^6?+=bA@@5B9Ao>MnHRa`(9HDPc+{Fmh96RO-{j;7M?R$PMB!)hdq*%@zJOy`(Gt!x->)mkD%KBrL@L>6-=~0ni-p@^TB~pd?fyMaDGf4Sm!5 z%7zaY&pxP8_rSDhEd44LP)dz^fre`BDT; z2dr<`WO~~yTn25V!G5}&Ay!5qs237g--EV$HR!R|xITTO-c{%AQ~V6CCN$r18Ym_d zZeiNrS`RfxP7di0QfWrJdC#NsP%R1n*M_Gwqx{%j zu`zNOD@q|_s?KTr=ufxmvIB(+7_;j@|Kwk}N#lpsvsPM}z}b6L@qclszH+euny<&U z&z|zuB?O|L6DUN0{J)YWepL0sr%n1oUdIg`Ql^fBqkA#JA35~M0j5{N4HnZ`qYiU( zY|>2x(sr#a`7h4obQQTUrstR1Uf8N@9Lx5!Lek1M@OJ|0=x zLqq*5o`vW9(%4bCJrO7CV3B|`k$Y}sA<$?0^Ner7GTAR6NDD~9KyP^?TY&(|HNS%c zE70`xdVc74{_iP5-uV44-kOl-M~geCk15%I4KBHv_;$Yzp;|$FKl3MPimv1qaE4Pj zdn!u>^h?uZQ*a!^}w$ZmFetv!1UejQ0 zJHs#I82kg76`Rq(I-OSTqME#YFh}~nr&MSix?;y+z@ATlXkj0L_Uf=WWi()M@G3}l zw0@^X>VwVK#(&Uqc2#+_8Rzsz9TNtn0qg?F(J|9oHbBmZYcsb1qaZ%t<=U0~MyG`r z`W;WzF$;#yRj_)jc%YP&xoV^ntaR8du>995+=EaS&*f-cgx} zFMH*AMV&=FBgk~uMnTb&Ocp@Qr63$Gy3n-2+Gl<6-JvqBx7O_&y}N)eH4Y-ErGy^u?Mb0|@!2}l8l zBnDHpv5@=q@|)1g22tk^Kc9}a9Y>eA&R``Denwvf;HQ0_*VDMQ%_sG7QUmuKrncye ziPCRxUodUj{-HCLhVRjb?GO{j^i-M;9Efp9vBEf{;OO;nJ$L50J3>JZ{$8I%MUG$; ztJuIA)SA$>C<_b;1!BMaU8=n|YV5B1`Q=Yc7z~u4YW9?hSPk%fEDH12KzD~rCIu<7b=`EvhV7q=lEVNyNNni4P!m2P9Pfgm-|KCy6jI1$Ex zd^oc#Mq^!hu@_FFkJImy?S68-?PUZ5-^yvyWh@B7_1NOg=MaBy z-B!3s)jX8UYWpq?7zkq5#=)?^m6%2qm*PeWQMjnuK}cqwNJ$Upa>>L@sA(ThXmuYM z=m0hK{O(u)v zhGCjM|BUfLzs&z;W}?by;Nx?qUcz8+6#Fb(4yJz^S(f1@m;4#0ONC8g5#N+sATO{q)Q)9@U(ciYBNn#zxELIs~C z(ER?$bY^yA(;$;y@H=JqzA(siVFyncAP{BqyKVh)Sr}mHH|fP_1d09+FG7gyX#R?R zrlgJW`%nPeXVU&%N-(Swng-1n4mGy(6b*FMuUfESL?AU+`NuU$qzEHHpuuFuf8*?V zZaTzr=BQHkiy?pN`40EvhlEZ{5Isg$s8Qv~5E;h#lP9o(pk7TFin%|$VV;I5riV0? z#Eg~Igs?41Y@n#kFX0a`(90hA%s!7PgwU=(zrU?Le_LnM=M~;vMH~@sG^Yd5lbWvJ!oV#0 z3v9&y5IJD|@DluAc|L>xUaS6m%gI&4<4I5B@a*r|nA)5kU`z``DWcA;K+>&j5N7%8 zMeavs1{qz|UrmqFTOFwLJ*%~~l8(9kmaGee03 z;9d~3zWJ4v4I?_XtR~Wg7O*k9`sz^Ba}&b<6+MW|*?1Uu>+s(`z)3IFTW{yEK=7jH zZVmaXlqH)HeBK8+jF6;{U{S;Fr+A1xr3mxEO&gcOrTR>+!ww9)8kEQ6VQT>~6%M*J zDP>deRHu+Gkp+MMlJ9!Y&m0yWl7ZSYi3*W;>T_3+OH`uft3+ohLGN`TmV}Ss=zxNk zLO>U|Iy7$5C7A=_|KaXp0r>z|$XO~g_P51WPeFgp`aOZ|#V77LkAvyvtEYIx=8F8Y zr`wO)u=CU|YjnO$JK0|gpd9x+Ht{&X+Fx@>jVb@l3{#a-OLuLbXc7DQlLf17i;K&JOo<<_{F zSFGvmVe`_aK{-B59Q)OqcAvTR2xlB^!s0N3sbXfTELMXqgSy`M->&RD1{_Ln-$Tw^ zZO@r%$U)vf1%eO}`&%(k;@QZ62{$`T(36B}bDdqyx#{?=-y;Q%P?{!s&iaj%Ns0LH zK|V?`44wkP9f2-5hZC4(y^J|dbn#)DKY1xtNWxN&-VvdVW&-8UXKw8Ly4wwWI;b@M z_)YxMeDC1+Gf;q~+?R3xpI29(Xa*;~4Lwqg-^%OVpJvvg@ zRss1yRY(@~$Zu#54>A%HmmF;OvjH#9Mt^7TOPX_6JW6wP8WQ-k^oHtNk^Q4vf{y^W z=`2Fnp~CTCFnEO-UhDiJ__e+*UCP--of@vEp+6(>zYd^?A1FBo;6&^tZm|kwZuJbo z0Xpvy?zNdWT&v>A=L4eAI*%BjBj4HVZ{;Fq`T!u4znt`$8jgIk<@7Y?J03Q^R55Me zzKo-zRXz*R_u?A6a4Ic+iLqTO%I@}8Bs@1F%`={Z|5cfUGAvm z>Vhe8OjP%hne-H-z+0;Zu^vSYub0*{sLnp2f9C$7ExAzh3O-eylv?m*^KZEFAket3 z3_}Nouxy2a8f;+LeA0Q2VMj%+%Yz2Fsa;n%m|NM%N0z zqk;FJ^-zg?I0C@{-D7GbdW|yxHhprPP7g(czi9OCyeK=a_mX1unFYaXF5!)UMIr4* zd;uF!7+ap_dMa|rd8YN5epOMx!<#-Z)rVlO49sji(}>l7f`Wv6wIFuiO+@D0dxc;F zY6Ys7SwQ%!Nmu~*a0~|#{-4sCiq)r3GB6DET|W7Zz)%lu8PE0jFh)rY;1o8s0KCC~ zzxMx$HyA1iRT|R`b+)044-^N5Q1Uy$0iVw#0Q^RDc1pGV0SgRaJP5QE#m GBL5GQxj>8n literal 0 HcmV?d00001 diff --git a/ddms/libs/Android.mk b/ddms/libs/Android.mk new file mode 100644 index 000000000..c62c6d05e --- /dev/null +++ b/ddms/libs/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2007 The Android Open Source Project +# +DDMSLIBS_LOCAL_DIR := $(call my-dir) +include $(DDMSLIBS_LOCAL_DIR)/ddmlib/Android.mk +include $(DDMSLIBS_LOCAL_DIR)/ddmuilib/Android.mk diff --git a/ddms/libs/ddmlib/.classpath b/ddms/libs/ddmlib/.classpath new file mode 100644 index 000000000..fb5011632 --- /dev/null +++ b/ddms/libs/ddmlib/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/ddms/libs/ddmlib/.project b/ddms/libs/ddmlib/.project new file mode 100644 index 000000000..fea25c769 --- /dev/null +++ b/ddms/libs/ddmlib/.project @@ -0,0 +1,17 @@ + + + ddmlib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/ddms/libs/ddmlib/Android.mk b/ddms/libs/ddmlib/Android.mk new file mode 100644 index 000000000..a49bdd2f4 --- /dev/null +++ b/ddms/libs/ddmlib/Android.mk @@ -0,0 +1,4 @@ +# Copyright 2007 The Android Open Source Project +# +DDMLIB_LOCAL_DIR := $(call my-dir) +include $(DDMLIB_LOCAL_DIR)/src/Android.mk diff --git a/ddms/libs/ddmlib/src/Android.mk b/ddms/libs/ddmlib/src/Android.mk new file mode 100644 index 000000000..da07f9791 --- /dev/null +++ b/ddms/libs/ddmlib/src/Android.mk @@ -0,0 +1,11 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE := ddmlib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java new file mode 100644 index 000000000..ce8d36626 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java @@ -0,0 +1,731 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.log.LogReceiver; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; + +/** + * Helper class to handle requests and connections to adb. + *

{@link DebugBridgeServer} is the public API to connection to adb, while {@link AdbHelper} + * does the low level stuff. + *

This currently uses spin-wait non-blocking I/O. A Selector would be more efficient, + * but seems like overkill for what we're doing here. + */ +final class AdbHelper { + + // public static final long kOkay = 0x59414b4fL; + // public static final long kFail = 0x4c494146L; + + static final int WAIT_TIME = 5; // spin-wait sleep, in ms + + static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$ + + /** do not instantiate */ + private AdbHelper() { + } + + /** + * Response from ADB. + */ + static class AdbResponse { + public AdbResponse() { + // ioSuccess = okay = timeout = false; + message = ""; + } + + public boolean ioSuccess; // read all expected data, no timeoutes + + public boolean okay; // first 4 bytes in response were "OKAY"? + + public boolean timeout; // TODO: implement + + public String message; // diagnostic string + } + + /** + * Create and connect a new pass-through socket, from the host to a port on + * the device. + * + * @param adbSockAddr + * @param device the device to connect to. Can be null in which case the connection will be + * to the first available device. + * @param devicePort the port we're opening + */ + public static SocketChannel open(InetSocketAddress adbSockAddr, + Device device, int devicePort) throws IOException { + + SocketChannel adbChan = SocketChannel.open(adbSockAddr); + try { + adbChan.socket().setTcpNoDelay(true); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk to a specific device + setDevice(adbChan, device); + + byte[] req = createAdbForwardRequest(null, devicePort); + // Log.hexDump(req); + + if (write(adbChan, req) == false) + throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$ + + AdbResponse resp = readAdbResponse(adbChan, false); + if (!resp.okay) + throw new IOException("connection request rejected"); //$NON-NLS-1$ + + adbChan.configureBlocking(true); + } catch (IOException ioe) { + adbChan.close(); + throw ioe; + } + + return adbChan; + } + + /** + * Creates and connects a new pass-through socket, from the host to a port on + * the device. + * + * @param adbSockAddr + * @param device the device to connect to. Can be null in which case the connection will be + * to the first available device. + * @param pid the process pid to connect to. + */ + public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr, + Device device, int pid) throws IOException { + + SocketChannel adbChan = SocketChannel.open(adbSockAddr); + try { + adbChan.socket().setTcpNoDelay(true); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk to a specific device + setDevice(adbChan, device); + + byte[] req = createJdwpForwardRequest(pid); + // Log.hexDump(req); + + if (write(adbChan, req) == false) + throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$ + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) + throw new IOException("connection request rejected: " + resp.message); //$NON-NLS-1$ + + adbChan.configureBlocking(true); + } catch (IOException ioe) { + adbChan.close(); + throw ioe; + } + + return adbChan; + } + + /** + * Creates a port forwarding request for adb. This returns an array + * containing "####tcp:{port}:{addStr}". + * @param addrStr the host. Can be null. + * @param port the port on the device. This does not need to be numeric. + */ + private static byte[] createAdbForwardRequest(String addrStr, int port) { + String reqStr; + + if (addrStr == null) + reqStr = "tcp:" + port; + else + reqStr = "tcp:" + port + ":" + addrStr; + return formAdbRequest(reqStr); + } + + /** + * Creates a port forwarding request to a jdwp process. This returns an array + * containing "####jwdp:{pid}". + * @param pid the jdwp process pid on the device. + */ + private static byte[] createJdwpForwardRequest(int pid) { + String reqStr = String.format("jdwp:%1$d", pid); //$NON-NLS-1$ + return formAdbRequest(reqStr); + } + + /** + * Create an ASCII string preceeded by four hex digits. The opening "####" + * is the length of the rest of the string, encoded as ASCII hex (case + * doesn't matter). "port" and "host" are what we want to forward to. If + * we're on the host side connecting into the device, "addrStr" should be + * null. + */ + static byte[] formAdbRequest(String req) { + String resultStr = String.format("%04X%s", req.length(), req); //$NON-NLS-1$ + byte[] result; + try { + result = resultStr.getBytes(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + return null; + } + assert result.length == req.length() + 4; + return result; + } + + /** + * Reads the response from ADB after a command. + * @param chan The socket channel that is connected to adb. + * @param readDiagString If true, we're expecting an OKAY response to be + * followed by a diagnostic string. Otherwise, we only expect the + * diagnostic string to follow a FAIL. + */ + static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString) + throws IOException { + + AdbResponse resp = new AdbResponse(); + + byte[] reply = new byte[4]; + if (read(chan, reply) == false) { + return resp; + } + resp.ioSuccess = true; + + if (isOkay(reply)) { + resp.okay = true; + } else { + readDiagString = true; // look for a reason after the FAIL + resp.okay = false; + } + + // not a loop -- use "while" so we can use "break" + while (readDiagString) { + // length string is in next 4 bytes + byte[] lenBuf = new byte[4]; + if (read(chan, lenBuf) == false) { + Log.w("ddms", "Expected diagnostic string not found"); + break; + } + + String lenStr = replyToString(lenBuf); + + int len; + try { + len = Integer.parseInt(lenStr, 16); + } catch (NumberFormatException nfe) { + Log.w("ddms", "Expected digits, got '" + lenStr + "': " + + lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " " + + lenBuf[3]); + Log.w("ddms", "reply was " + replyToString(reply)); + break; + } + + byte[] msg = new byte[len]; + if (read(chan, msg) == false) { + Log.w("ddms", "Failed reading diagnostic string, len=" + len); + break; + } + + resp.message = replyToString(msg); + Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='" + + resp.message + "'"); + + break; + } + + return resp; + } + + /** + * Retrieve the frame buffer from the device. + */ + public static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device) + throws IOException { + + RawImage imageParams = new RawImage(); + byte[] request = formAdbRequest("framebuffer:"); //$NON-NLS-1$ + byte[] nudge = { + 0 + }; + byte[] reply; + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + if (write(adbChan, request) == false) + throw new IOException("failed asking for frame buffer"); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + Log.w("ddms", "Got timeout or unhappy response from ADB fb req: " + + resp.message); + adbChan.close(); + return null; + } + + // first the protocol version. + reply = new byte[4]; + if (read(adbChan, reply) == false) { + Log.w("ddms", "got partial reply from ADB fb:"); + Log.hexDump("ddms", LogLevel.WARN, reply, 0, reply.length); + adbChan.close(); + return null; + } + ByteBuffer buf = ByteBuffer.wrap(reply); + buf.order(ByteOrder.LITTLE_ENDIAN); + + int version = buf.getInt(); + + // get the header size (this is a count of int) + int headerSize = RawImage.getHeaderSize(version); + + // read the header + reply = new byte[headerSize * 4]; + if (read(adbChan, reply) == false) { + Log.w("ddms", "got partial reply from ADB fb:"); + Log.hexDump("ddms", LogLevel.WARN, reply, 0, reply.length); + adbChan.close(); + return null; + } + buf = ByteBuffer.wrap(reply); + buf.order(ByteOrder.LITTLE_ENDIAN); + + // fill the RawImage with the header + if (imageParams.readHeader(version, buf) == false) { + Log.e("Screenshot", "Unsupported protocol: " + version); + return null; + } + + Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size=" + + imageParams.size + ", width=" + imageParams.width + + ", height=" + imageParams.height); + + if (write(adbChan, nudge) == false) + throw new IOException("failed nudging"); + + reply = new byte[imageParams.size]; + if (read(adbChan, reply) == false) { + Log.w("ddms", "got truncated reply from ADB fb data"); + adbChan.close(); + return null; + } + + imageParams.data = reply; + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + + return imageParams; + } + + /** + * Execute a command on the device and retrieve the output. The output is + * handed to "rcvr" as it arrives. + */ + public static void executeRemoteCommand(InetSocketAddress adbSockAddr, + String command, Device device, IShellOutputReceiver rcvr) + throws IOException { + Log.v("ddms", "execute: running " + command); + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to + // talk + // to a specific device + setDevice(adbChan, device); + + byte[] request = formAdbRequest("shell:" + command); //$NON-NLS-1$ + if (write(adbChan, request) == false) + throw new IOException("failed submitting shell command"); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message); + throw new IOException("sad result from adb: " + resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + Log.v("ddms", "execute: cancelled"); + break; + } + + count = adbChan.read(buf); + if (count < 0) { + // we're at the end, we flush the output + rcvr.flush(); + Log.v("ddms", "execute '" + command + "' on '" + device + "' : EOF hit. Read: " + + count); + break; + } else if (count == 0) { + try { + Thread.sleep(WAIT_TIME * 5); + } catch (InterruptedException ie) { + } + } else { + if (rcvr != null) { + rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); + } + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + Log.v("ddms", "execute: returning"); + } + } + + /** + * Runs the Event log service on the {@link Device}, and provides its output to the + * {@link LogReceiver}. + * @param adbSockAddr the socket address to connect to adb + * @param device the Device on which to run the service + * @param rcvr the {@link LogReceiver} to receive the log output + * @throws IOException + */ + public static void runEventLogService(InetSocketAddress adbSockAddr, Device device, + LogReceiver rcvr) throws IOException { + runLogService(adbSockAddr, device, "events", rcvr); //$NON-NLS-1$ + } + + /** + * Runs a log service on the {@link Device}, and provides its output to the {@link LogReceiver}. + * @param adbSockAddr the socket address to connect to adb + * @param device the Device on which to run the service + * @param logName the name of the log file to output + * @param rcvr the {@link LogReceiver} to receive the log output + * @throws IOException + */ + public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName, + LogReceiver rcvr) throws IOException { + SocketChannel adbChan = null; + + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + byte[] request = formAdbRequest("log:" + logName); + if (write(adbChan, request) == false) { + throw new IOException("failed to submit the log command"); + } + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected log command: " + resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + break; + } + + count = adbChan.read(buf); + if (count < 0) { + break; + } else if (count == 0) { + try { + Thread.sleep(WAIT_TIME * 5); + } catch (InterruptedException ie) { + } + } else { + if (rcvr != null) { + rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); + } + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + } + + /** + * Creates a port forwarding between a local and a remote port. + * @param adbSockAddr the socket address to connect to adb + * @param device the device on which to do the port fowarding + * @param localPort the local port to forward + * @param remotePort the remote port. + * @return true if success. + * @throws IOException + */ + public static boolean createForward(InetSocketAddress adbSockAddr, Device device, int localPort, + int remotePort) throws IOException { + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + byte[] request = formAdbRequest(String.format( + "host-serial:%1$s:forward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ + device.getSerialNumber(), localPort, remotePort)); + + if (write(adbChan, request) == false) { + throw new IOException("failed to submit the forward command."); + } + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected command: " + resp.message); + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + + return true; + } + + /** + * Remove a port forwarding between a local and a remote port. + * @param adbSockAddr the socket address to connect to adb + * @param device the device on which to remove the port fowarding + * @param localPort the local port of the forward + * @param remotePort the remote port. + * @return true if success. + * @throws IOException + */ + public static boolean removeForward(InetSocketAddress adbSockAddr, Device device, int localPort, + int remotePort) throws IOException { + + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + byte[] request = formAdbRequest(String.format( + "host-serial:%1$s:killforward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ + device.getSerialNumber(), localPort, remotePort)); + + if (!write(adbChan, request)) { + throw new IOException("failed to submit the remove forward command."); + } + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected command: " + resp.message); + } + } finally { + if (adbChan != null) { + adbChan.close(); + } + } + + return true; + } + + /** + * Checks to see if the first four bytes in "reply" are OKAY. + */ + static boolean isOkay(byte[] reply) { + return reply[0] == (byte)'O' && reply[1] == (byte)'K' + && reply[2] == (byte)'A' && reply[3] == (byte)'Y'; + } + + /** + * Converts an ADB reply to a string. + */ + static String replyToString(byte[] reply) { + String result; + try { + result = new String(reply, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + result = ""; + } + return result; + } + + /** + * Reads from the socket until the array is filled, or no more data is coming (because + * the socket closed or the timeout expired). + * + * @param chan the opened socket to read from. It must be in non-blocking + * mode for timeouts to work + * @param data the buffer to store the read data into. + * @return "true" if all data was read. + * @throws IOException + */ + static boolean read(SocketChannel chan, byte[] data) { + try { + read(chan, data, -1, DdmPreferences.getTimeOut()); + } catch (IOException e) { + Log.d("ddms", "readAll: IOException: " + e.getMessage()); + return false; + } + + return true; + } + + /** + * Reads from the socket until the array is filled, the optional length + * is reached, or no more data is coming (because the socket closed or the + * timeout expired). After "timeout" milliseconds since the + * previous successful read, this will return whether or not new data has + * been found. + * + * @param chan the opened socket to read from. It must be in non-blocking + * mode for timeouts to work + * @param data the buffer to store the read data into. + * @param length the length to read or -1 to fill the data buffer completely + * @param timeout The timeout value. A timeout of zero means "wait forever". + * @throws IOException + */ + static void read(SocketChannel chan, byte[] data, int length, int timeout) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); + int numWaits = 0; + + while (buf.position() != buf.limit()) { + int count; + + count = chan.read(buf); + if (count < 0) { + Log.d("ddms", "read: channel EOF"); + throw new IOException("EOF"); + } else if (count == 0) { + // TODO: need more accurate timeout? + if (timeout != 0 && numWaits * WAIT_TIME > timeout) { + Log.d("ddms", "read: timeout"); + throw new IOException("timeout"); + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + } + } + + /** + * Write until all data in "data" is written or the connection fails. + * @param chan the opened socket to write to. + * @param data the buffer to send. + * @return "true" if all data was written. + */ + static boolean write(SocketChannel chan, byte[] data) { + try { + write(chan, data, -1, DdmPreferences.getTimeOut()); + } catch (IOException e) { + Log.e("ddms", e); + return false; + } + + return true; + } + + /** + * Write until all data in "data" is written, the optional length is reached, + * the timeout expires, or the connection fails. Returns "true" if all + * data was written. + * @param chan the opened socket to write to. + * @param data the buffer to send. + * @param length the length to write or -1 to send the whole buffer. + * @param timeout The timeout value. A timeout of zero means "wait forever". + * @throws IOException + */ + static void write(SocketChannel chan, byte[] data, int length, int timeout) + throws IOException { + ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); + int numWaits = 0; + + while (buf.position() != buf.limit()) { + int count; + + count = chan.write(buf); + if (count < 0) { + Log.d("ddms", "write: channel EOF"); + throw new IOException("channel EOF"); + } else if (count == 0) { + // TODO: need more accurate timeout? + if (timeout != 0 && numWaits * WAIT_TIME > timeout) { + Log.d("ddms", "write: timeout"); + throw new IOException("timeout"); + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + } + } + + /** + * tells adb to talk to a specific device + * + * @param adbChan the socket connection to adb + * @param device The device to talk to. + * @throws IOException + */ + static void setDevice(SocketChannel adbChan, Device device) + throws IOException { + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + if (device != null) { + String msg = "host:transport:" + device.getSerialNumber(); //$NON-NLS-1$ + byte[] device_query = formAdbRequest(msg); + + if (write(adbChan, device_query) == false) + throw new IOException("failed submitting device (" + device + + ") request to ADB"); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.okay) + throw new IOException("device (" + device + + ") request rejected: " + resp.message); + } + + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java new file mode 100644 index 000000000..c6d4b50ad --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java @@ -0,0 +1,71 @@ +/* + * 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.ddmlib; + +/** + * Holds an Allocation information. + */ +public class AllocationInfo implements Comparable, IStackTraceInfo { + private String mAllocatedClass; + private int mAllocationSize; + private short mThreadId; + private StackTraceElement[] mStackTrace; + + /* + * Simple constructor. + */ + AllocationInfo(String allocatedClass, int allocationSize, + short threadId, StackTraceElement[] stackTrace) { + mAllocatedClass = allocatedClass; + mAllocationSize = allocationSize; + mThreadId = threadId; + mStackTrace = stackTrace; + } + + /** + * Returns the name of the allocated class. + */ + public String getAllocatedClass() { + return mAllocatedClass; + } + + /** + * Returns the size of the allocation. + */ + public int getSize() { + return mAllocationSize; + } + + /** + * Returns the id of the thread that performed the allocation. + */ + public short getThreadId() { + return mThreadId; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IStackTraceInfo#getStackTrace() + */ + public StackTraceElement[] getStackTrace() { + return mStackTrace; + } + + public int compareTo(AllocationInfo otherAlloc) { + return otherAlloc.mAllocationSize - mAllocationSize; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java new file mode 100644 index 000000000..6b9dccc7e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java @@ -0,0 +1,1052 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.Log.LogLevel; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.Thread.State; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A connection to the host-side android debug bridge (adb) + *

This is the central point to communicate with any devices, emulators, or the applications + * running on them. + *

{@link #init(boolean)} must be called before anything is done. + */ +public final class AndroidDebugBridge { + + /* + * Minimum and maximum version of adb supported. This correspond to + * ADB_SERVER_VERSION found in //device/tools/adb/adb.h + */ + + private final static int ADB_VERSION_MICRO_MIN = 20; + private final static int ADB_VERSION_MICRO_MAX = -1; + + private final static Pattern sAdbVersion = Pattern.compile( + "^.*(\\d+)\\.(\\d+)\\.(\\d+)$"); //$NON-NLS-1$ + + private final static String ADB = "adb"; //$NON-NLS-1$ + private final static String DDMS = "ddms"; //$NON-NLS-1$ + + // Where to find the ADB bridge. + final static String ADB_HOST = "127.0.0.1"; //$NON-NLS-1$ + final static int ADB_PORT = 5037; + + static InetAddress sHostAddr; + static InetSocketAddress sSocketAddr; + + static { + // built-in local address/port for ADB. + try { + sHostAddr = InetAddress.getByName(ADB_HOST); + sSocketAddr = new InetSocketAddress(sHostAddr, ADB_PORT); + } catch (UnknownHostException e) { + + } + } + + private static AndroidDebugBridge sThis; + private static boolean sClientSupport; + + /** Full path to adb. */ + private String mAdbOsLocation = null; + + private boolean mVersionCheck; + + private boolean mStarted = false; + + private DeviceMonitor mDeviceMonitor; + + private final static ArrayList sBridgeListeners = + new ArrayList(); + private final static ArrayList sDeviceListeners = + new ArrayList(); + private final static ArrayList sClientListeners = + new ArrayList(); + + // lock object for synchronization + private static final Object sLock = sBridgeListeners; + + /** + * Classes which implement this interface provide a method that deals + * with {@link AndroidDebugBridge} changes. + */ + public interface IDebugBridgeChangeListener { + /** + * Sent when a new {@link AndroidDebugBridge} is connected. + *

+ * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + */ + public void bridgeChanged(AndroidDebugBridge bridge); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link IDevice} addition, deletion, and changes. + */ + public interface IDeviceChangeListener { + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + */ + public void deviceConnected(IDevice device); + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + */ + public void deviceDisconnected(IDevice device); + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + *

+ * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask describing what changed. It can contain any of the following + * values: {@link IDevice#CHANGE_BUILD_INFO}, {@link IDevice#CHANGE_STATE}, + * {@link IDevice#CHANGE_CLIENT_LIST} + */ + public void deviceChanged(IDevice device, int changeMask); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link Client} changes. + */ + public interface IClientChangeListener { + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + */ + public void clientChanged(Client client, int changeMask); + } + + /** + * Initializes the ddm library. + *

This must be called once before any call to + * {@link #createBridge(String, boolean)}. + *

The library can be initialized in 2 ways: + *

    + *
  • Mode 1: clientSupport == true.
    The library monitors the + * devices and the applications running on them. It will connect to each application, as a + * debugger of sort, to be able to interact with them through JDWP packets.
  • + *
  • Mode 2: clientSupport == false.
    The library only monitors + * devices. The applications are left untouched, letting other tools built on + * ddmlib to connect a debugger to them.
  • + *
+ *

Only one tool can run in mode 1 at the same time. + *

Note that mode 1 does not prevent debugging of applications running on devices. Mode 1 + * lets debuggers connect to ddmlib which acts as a proxy between the debuggers and + * the applications to debug. See {@link Client#getDebuggerListenPort()}. + *

The preferences of ddmlib should also be initialized with whatever default + * values were changed from the default values. + *

When the application quits, {@link #terminate()} should be called. + * @param clientSupport Indicates whether the library should enable the monitoring and + * interaction with applications running on the devices. + * @see AndroidDebugBridge#createBridge(String, boolean) + * @see DdmPreferences + */ + public static void init(boolean clientSupport) { + sClientSupport = clientSupport; + + MonitorThread monitorThread = MonitorThread.createInstance(); + monitorThread.start(); + + HandleHello.register(monitorThread); + HandleAppName.register(monitorThread); + HandleTest.register(monitorThread); + HandleThread.register(monitorThread); + HandleHeap.register(monitorThread); + HandleWait.register(monitorThread); + HandleProfiling.register(monitorThread); + } + + /** + * Terminates the ddm library. This must be called upon application termination. + */ + public static void terminate() { + // kill the monitoring services + if (sThis != null && sThis.mDeviceMonitor != null) { + sThis.mDeviceMonitor.stop(); + sThis.mDeviceMonitor = null; + } + + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.quit(); + } + } + + /** + * Returns whether the ddmlib is setup to support monitoring and interacting with + * {@link Client}s running on the {@link IDevice}s. + */ + static boolean getClientSupport() { + return sClientSupport; + } + + /** + * Creates a {@link AndroidDebugBridge} that is not linked to any particular executable. + *

This bridge will expect adb to be running. It will not be able to start/stop/restart + * adb. + *

If a bridge has already been started, it is directly returned with no changes (similar + * to calling {@link #getBridge()}). + * @return a connected bridge. + */ + public static AndroidDebugBridge createBridge() { + synchronized (sLock) { + if (sThis != null) { + return sThis; + } + + try { + sThis = new AndroidDebugBridge(); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + + return sThis; + } + } + + + /** + * Creates a new debug bridge from the location of the command line tool. + *

+ * Any existing server will be disconnected, unless the location is the same and + * forceNewBridge is set to false. + * @param osLocation the location of the command line tool 'adb' + * @param forceNewBridge force creation of a new bridge even if one with the same location + * already exists. + * @return a connected bridge. + */ + public static AndroidDebugBridge createBridge(String osLocation, boolean forceNewBridge) { + synchronized (sLock) { + if (sThis != null) { + if (sThis.mAdbOsLocation != null && sThis.mAdbOsLocation.equals(osLocation) && + forceNewBridge == false) { + return sThis; + } else { + // stop the current server + sThis.stop(); + } + } + + try { + sThis = new AndroidDebugBridge(osLocation); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + + return sThis; + } + } + + /** + * Returns the current debug bridge. Can be null if none were created. + */ + public static AndroidDebugBridge getBridge() { + return sThis; + } + + /** + * Disconnects the current debug bridge, and destroy the object. + *

This also stops the current adb host server. + *

+ * A new object will have to be created with {@link #createBridge(String, boolean)}. + */ + public static void disconnectBridge() { + synchronized (sLock) { + if (sThis != null) { + sThis.stop(); + sThis = null; + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners. + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a new + * {@link AndroidDebugBridge} is connected, by sending it one of the messages defined + * in the {@link IDebugBridgeChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + if (sBridgeListeners.contains(listener) == false) { + sBridgeListeners.add(listener); + if (sThis != null) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a new + * {@link AndroidDebugBridge} is started. + * @param listener The listener which should no longer be notified. + */ + public static void removeDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + sBridgeListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link IDevice} + * is connected, disconnected, or when its properties or its {@link Client} list changed, + * by sending it one of the messages defined in the {@link IDeviceChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + if (sDeviceListeners.contains(listener) == false) { + sDeviceListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link IDevice} is connected, disconnected, or when its properties or its {@link Client} + * list changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + sDeviceListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link Client} + * property changed, by sending it one of the messages defined in the + * {@link IClientChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addClientChangeListener(IClientChangeListener listener) { + synchronized (sLock) { + if (sClientListeners.contains(listener) == false) { + sClientListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link Client} property changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeClientChangeListener(IClientChangeListener listener) { + synchronized (sLock) { + sClientListeners.remove(listener); + } + } + + + /** + * Returns the devices. + * @see #hasInitialDeviceList() + */ + public IDevice[] getDevices() { + synchronized (sLock) { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getDevices(); + } + } + + return new IDevice[0]; + } + + /** + * Returns whether the bridge has acquired the initial list from adb after being created. + *

Calling {@link #getDevices()} right after {@link #createBridge(String, boolean)} will + * generally result in an empty list. This is due to the internal asynchronous communication + * mechanism with adb that does not guarantee that the {@link IDevice} list has been + * built before the call to {@link #getDevices()}. + *

The recommended way to get the list of {@link IDevice} objects is to create a + * {@link IDeviceChangeListener} object. + */ + public boolean hasInitialDeviceList() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.hasInitialDeviceList(); + } + + return false; + } + + /** + * Sets the client to accept debugger connection on the custom "Selected debug port". + * @param selectedClient the client. Can be null. + */ + public void setSelectedClient(Client selectedClient) { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setSelectedClient(selectedClient); + } + } + + /** + * Returns whether the {@link AndroidDebugBridge} object is still connected to the adb daemon. + */ + public boolean isConnected() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (mDeviceMonitor != null && monitorThread != null) { + return mDeviceMonitor.isMonitoring() && monitorThread.getState() != State.TERMINATED; + } + return false; + } + + /** + * Returns the number of times the {@link AndroidDebugBridge} object attempted to connect + * to the adb daemon. + */ + public int getConnectionAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getConnectionAttemptCount(); + } + return -1; + } + + /** + * Returns the number of times the {@link AndroidDebugBridge} object attempted to restart + * the adb daemon. + */ + public int getRestartAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getRestartAttemptCount(); + } + return -1; + } + + /** + * Creates a new bridge. + * @param osLocation the location of the command line tool + * @throws InvalidParameterException + */ + private AndroidDebugBridge(String osLocation) throws InvalidParameterException { + if (osLocation == null || osLocation.length() == 0) { + throw new InvalidParameterException(); + } + mAdbOsLocation = osLocation; + + checkAdbVersion(); + } + + /** + * Creates a new bridge not linked to any particular adb executable. + */ + private AndroidDebugBridge() { + } + + /** + * Queries adb for its version number and checks it against {@link #MIN_VERSION_NUMBER} and + * {@link #MAX_VERSION_NUMBER} + */ + private void checkAdbVersion() { + // default is bad check + mVersionCheck = false; + + if (mAdbOsLocation == null) { + return; + } + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "version"; //$NON-NLS-1$ + Log.d(DDMS, String.format("Checking '%1$s version'", mAdbOsLocation)); //$NON-NLS-1$ + Process process = Runtime.getRuntime().exec(command); + + ArrayList errorOutput = new ArrayList(); + ArrayList stdOutput = new ArrayList(); + int status = grabProcessOutput(process, errorOutput, stdOutput, + true /* waitForReaders */); + + if (status != 0) { + StringBuilder builder = new StringBuilder("'adb version' failed!"); //$NON-NLS-1$ + for (String error : errorOutput) { + builder.append('\n'); + builder.append(error); + } + Log.logAndDisplay(LogLevel.ERROR, "adb", builder.toString()); + } + + // check both stdout and stderr + boolean versionFound = false; + for (String line : stdOutput) { + versionFound = scanVersionLine(line); + if (versionFound) { + break; + } + } + if (!versionFound) { + for (String line : errorOutput) { + versionFound = scanVersionLine(line); + if (versionFound) { + break; + } + } + } + + if (!versionFound) { + // if we get here, we failed to parse the output. + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Failed to parse the output of 'adb version'"); //$NON-NLS-1$ + } + + } catch (IOException e) { + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Failed to get the adb version: " + e.getMessage()); //$NON-NLS-1$ + } catch (InterruptedException e) { + } finally { + + } + } + + /** + * Scans a line resulting from 'adb version' for a potential version number. + *

+ * If a version number is found, it checks the version number against what is expected + * by this version of ddms. + *

+ * Returns true when a version number has been found so that we can stop scanning, + * whether the version number is in the acceptable range or not. + * + * @param line The line to scan. + * @return True if a version number was found (whether it is acceptable or not). + */ + private boolean scanVersionLine(String line) { + if (line != null) { + Matcher matcher = sAdbVersion.matcher(line); + if (matcher.matches()) { + int majorVersion = Integer.parseInt(matcher.group(1)); + int minorVersion = Integer.parseInt(matcher.group(2)); + int microVersion = Integer.parseInt(matcher.group(3)); + + // check only the micro version for now. + if (microVersion < ADB_VERSION_MICRO_MIN) { + String message = String.format( + "Required minimum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$ + + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$ + majorVersion, minorVersion, ADB_VERSION_MICRO_MIN, + microVersion); + Log.logAndDisplay(LogLevel.ERROR, ADB, message); + } else if (ADB_VERSION_MICRO_MAX != -1 && + microVersion > ADB_VERSION_MICRO_MAX) { + String message = String.format( + "Required maximum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$ + + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$ + majorVersion, minorVersion, ADB_VERSION_MICRO_MAX, + microVersion); + Log.logAndDisplay(LogLevel.ERROR, ADB, message); + } else { + mVersionCheck = true; + } + + return true; + } + } + return false; + } + + /** + * Starts the debug bridge. + * @return true if success. + */ + boolean start() { + if (mAdbOsLocation != null && (mVersionCheck == false || startAdb() == false)) { + return false; + } + + mStarted = true; + + // now that the bridge is connected, we start the underlying services. + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + + return true; + } + + /** + * Kills the debug bridge, and the adb host server. + * @return true if success + */ + boolean stop() { + // if we haven't started we return false; + if (mStarted == false) { + return false; + } + + // kill the monitoring services + mDeviceMonitor.stop(); + mDeviceMonitor = null; + + if (stopAdb() == false) { + return false; + } + + mStarted = false; + return true; + } + + /** + * Restarts adb, but not the services around it. + * @return true if success. + */ + public boolean restart() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot restart adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + if (mVersionCheck == false) { + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Attempting to restart adb, but version check failed!"); //$NON-NLS-1$ + return false; + } + synchronized (this) { + stopAdb(); + + boolean restart = startAdb(); + + if (restart && mDeviceMonitor == null) { + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + } + + return restart; + } + } + + /** + * Notify the listener of a new {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the new IDevice. + * @see #getLock() + */ + void deviceConnected(IDevice device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceConnected(device); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a disconnected {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the disconnected IDevice. + * @see #getLock() + */ + void deviceDisconnected(IDevice device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceDisconnected(device); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a modified {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the modified IDevice. + * @see #getLock() + */ + void deviceChanged(IDevice device, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceChanged(device, changeMask); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a modified {@link Client}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the modified Client. + * @param changeMask the mask indicating what changed in the Client + * @see #getLock() + */ + void clientChanged(Client client, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IClientChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sClientListeners.toArray( + new IClientChangeListener[sClientListeners.size()]); + + } + + // Notify the listeners + for (IClientChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.clientChanged(client, changeMask); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Returns the {@link DeviceMonitor} object. + */ + DeviceMonitor getDeviceMonitor() { + return mDeviceMonitor; + } + + /** + * Starts the adb host side server. + * @return true if success + */ + synchronized boolean startAdb() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot start adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "start-server"; //$NON-NLS-1$ + Log.d(DDMS, + String.format("Launching '%1$s %2$s' to ensure ADB is running.", //$NON-NLS-1$ + mAdbOsLocation, command[1])); + proc = Runtime.getRuntime().exec(command); + + ArrayList errorOutput = new ArrayList(); + ArrayList stdOutput = new ArrayList(); + status = grabProcessOutput(proc, errorOutput, stdOutput, + false /* waitForReaders */); + + } catch (IOException ioe) { + Log.d(DDMS, "Unable to run 'adb': " + ioe.getMessage()); //$NON-NLS-1$ + // we'll return false; + } catch (InterruptedException ie) { + Log.d(DDMS, "Unable to run 'adb': " + ie.getMessage()); //$NON-NLS-1$ + // we'll return false; + } + + if (status != 0) { + Log.w(DDMS, + "'adb start-server' failed -- run manually if necessary"); //$NON-NLS-1$ + return false; + } + + Log.d(DDMS, "'adb start-server' succeeded"); //$NON-NLS-1$ + + return true; + } + + /** + * Stops the adb host side server. + * @return true if success + */ + private synchronized boolean stopAdb() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot stop adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "kill-server"; //$NON-NLS-1$ + proc = Runtime.getRuntime().exec(command); + status = proc.waitFor(); + } + catch (IOException ioe) { + // we'll return false; + } + catch (InterruptedException ie) { + // we'll return false; + } + + if (status != 0) { + Log.w(DDMS, + "'adb kill-server' failed -- run manually if necessary"); //$NON-NLS-1$ + return false; + } + + Log.d(DDMS, "'adb kill-server' succeeded"); //$NON-NLS-1$ + return true; + } + + /** + * Get the stderr/stdout outputs of a process and return when the process is done. + * Both must be read or the process will block on windows. + * @param process The process to get the ouput from + * @param errorOutput The array to store the stderr output. cannot be null. + * @param stdOutput The array to store the stdout output. cannot be null. + * @param displayStdOut If true this will display stdout as well + * @param waitforReaders if true, this will wait for the reader threads. + * @return the process return code. + * @throws InterruptedException + */ + private int grabProcessOutput(final Process process, final ArrayList errorOutput, + final ArrayList stdOutput, boolean waitforReaders) + throws InterruptedException { + assert errorOutput != null; + assert stdOutput != null; + // read the lines as they come. if null is returned, it's + // because the process finished + Thread t1 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + Log.e(ADB, line); + errorOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + Thread t2 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + Log.d(ADB, line); + stdOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + t1.start(); + t2.start(); + + // it looks like on windows process#waitFor() can return + // before the thread have filled the arrays, so we wait for both threads and the + // process itself. + if (waitforReaders) { + try { + t1.join(); + } catch (InterruptedException e) { + } + try { + t2.join(); + } catch (InterruptedException e) { + } + } + + // get the return code from the process + return process.waitFor(); + } + + /** + * Returns the singleton lock used by this class to protect any access to the listener. + *

+ * This includes adding/removing listeners, but also notifying listeners of new bridges, + * devices, and clients. + */ + static Object getLock() { + return sLock; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java new file mode 100644 index 000000000..129b312ae --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java @@ -0,0 +1,35 @@ +/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java +** +** Copyright 2007, 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.ddmlib; + +/** + * Thrown if the contents of a packet are bad. + */ +@SuppressWarnings("serial") +class BadPacketException extends RuntimeException { + public BadPacketException() + { + super(); + } + + public BadPacketException(String msg) + { + super(msg); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java new file mode 100644 index 000000000..74fa31839 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Subclass this with a class that handles one or more chunk types. + */ +abstract class ChunkHandler { + + public static final int CHUNK_HEADER_LEN = 8; // 4-byte type, 4-byte len + public static final ByteOrder CHUNK_ORDER = ByteOrder.BIG_ENDIAN; + + public static final int CHUNK_FAIL = type("FAIL"); + + ChunkHandler() {} + + /** + * Client is ready. The monitor thread calls this method on all + * handlers when the client is determined to be DDM-aware (usually + * after receiving a HELO response.) + * + * The handler can use this opportunity to initialize client-side + * activity. Because there's a fair chance we'll want to send a + * message to the client, this method can throw an IOException. + */ + abstract void clientReady(Client client) throws IOException; + + /** + * Client has gone away. Can be used to clean up any resources + * associated with this client connection. + */ + abstract void clientDisconnected(Client client); + + /** + * Handle an incoming chunk. The data, of chunk type "type", begins + * at the start of "data" and continues to data.limit(). + * + * If "isReply" is set, then "msgId" will be the ID of the request + * we sent to the client. Otherwise, it's the ID generated by the + * client for this event. Note that it's possible to receive chunks + * in reply packets for which we are not registered. + * + * The handler may not modify the contents of "data". + */ + abstract void handleChunk(Client client, int type, + ByteBuffer data, boolean isReply, int msgId); + + /** + * Handle chunks not recognized by handlers. The handleChunk() method + * in sub-classes should call this if the chunk type isn't recognized. + */ + protected void handleUnknownChunk(Client client, int type, + ByteBuffer data, boolean isReply, int msgId) { + if (type == CHUNK_FAIL) { + int errorCode, msgLen; + String msg; + + errorCode = data.getInt(); + msgLen = data.getInt(); + msg = getString(data, msgLen); + Log.w("ddms", "WARNING: failure code=" + errorCode + " msg=" + msg); + } else { + Log.w("ddms", "WARNING: received unknown chunk " + name(type) + + ": len=" + data.limit() + ", reply=" + isReply + + ", msgId=0x" + Integer.toHexString(msgId)); + } + Log.w("ddms", " client " + client + ", handler " + this); + } + + + /** + * Utility function to copy a String out of a ByteBuffer. + * + * This is here because multiple chunk handlers can make use of it, + * and there's nowhere better to put it. + */ + static String getString(ByteBuffer buf, int len) { + char[] data = new char[len]; + for (int i = 0; i < len; i++) + data[i] = buf.getChar(); + return new String(data); + } + + /** + * Utility function to copy a String into a ByteBuffer. + */ + static void putString(ByteBuffer buf, String str) { + int len = str.length(); + for (int i = 0; i < len; i++) + buf.putChar(str.charAt(i)); + } + + /** + * Convert a 4-character string to a 32-bit type. + */ + static int type(String typeName) { + int val = 0; + + if (typeName.length() != 4) { + Log.e("ddms", "Type name must be 4 letter long"); + throw new RuntimeException("Type name must be 4 letter long"); + } + + for (int i = 0; i < 4; i++) { + val <<= 8; + val |= (byte) typeName.charAt(i); + } + + return val; + } + + /** + * Convert an integer type to a 4-character string. + */ + static String name(int type) { + char[] ascii = new char[4]; + + ascii[0] = (char) ((type >> 24) & 0xff); + ascii[1] = (char) ((type >> 16) & 0xff); + ascii[2] = (char) ((type >> 8) & 0xff); + ascii[3] = (char) (type & 0xff); + + return new String(ascii); + } + + /** + * Allocate a ByteBuffer with enough space to hold the JDWP packet + * header and one chunk header in addition to the demands of the + * chunk being created. + * + * "maxChunkLen" indicates the size of the chunk contents only. + */ + static ByteBuffer allocBuffer(int maxChunkLen) { + ByteBuffer buf = + ByteBuffer.allocate(JdwpPacket.JDWP_HEADER_LEN + 8 +maxChunkLen); + buf.order(CHUNK_ORDER); + return buf; + } + + /** + * Return the slice of the JDWP packet buffer that holds just the + * chunk data. + */ + static ByteBuffer getChunkDataBuf(ByteBuffer jdwpBuf) { + ByteBuffer slice; + + assert jdwpBuf.position() == 0; + + jdwpBuf.position(JdwpPacket.JDWP_HEADER_LEN + CHUNK_HEADER_LEN); + slice = jdwpBuf.slice(); + slice.order(CHUNK_ORDER); + jdwpBuf.position(0); + + return slice; + } + + /** + * Write the chunk header at the start of the chunk. + * + * Pass in the byte buffer returned by JdwpPacket.getPayload(). + */ + static void finishChunkPacket(JdwpPacket packet, int type, int chunkLen) { + ByteBuffer buf = packet.getPayload(); + + buf.putInt(0x00, type); + buf.putInt(0x04, chunkLen); + + packet.finishPacket(CHUNK_HEADER_LEN + chunkLen); + } + + /** + * Check that the client is opened with the proper debugger port for the + * specified application name, and if not, reopen it. + * @param client + * @param uiThread + * @param appName + * @return + */ + protected static Client checkDebuggerPortForAppName(Client client, String appName) { + IDebugPortProvider provider = DebugPortManager.getProvider(); + if (provider != null) { + Device device = client.getDeviceImpl(); + int newPort = provider.getPort(device, appName); + + if (newPort != IDebugPortProvider.NO_STATIC_PORT && + newPort != client.getDebuggerListenPort()) { + + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + DeviceMonitor deviceMonitor = bridge.getDeviceMonitor(); + if (deviceMonitor != null) { + deviceMonitor.addClientToDropAndReopen(client, newPort); + client = null; + } + } + } + } + + return client; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java new file mode 100644 index 000000000..59910264d --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java @@ -0,0 +1,837 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.ClientData.MethodProfilingStatus; +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.HashMap; + +/** + * This represents a single client, usually a DAlvik VM process. + *

This class gives access to basic client information, as well as methods to perform actions + * on the client. + *

More detailed information, usually updated in real time, can be access through the + * {@link ClientData} class. Each Client object has its own ClientData + * accessed through {@link #getClientData()}. + */ +public class Client { + + private static final int SERVER_PROTOCOL_VERSION = 1; + + /** Client change bit mask: application name change */ + public static final int CHANGE_NAME = 0x0001; + /** Client change bit mask: debugger status change */ + public static final int CHANGE_DEBUGGER_STATUS = 0x0002; + /** Client change bit mask: debugger port change */ + public static final int CHANGE_PORT = 0x0004; + /** Client change bit mask: thread update flag change */ + public static final int CHANGE_THREAD_MODE = 0x0008; + /** Client change bit mask: thread data updated */ + public static final int CHANGE_THREAD_DATA = 0x0010; + /** Client change bit mask: heap update flag change */ + public static final int CHANGE_HEAP_MODE = 0x0020; + /** Client change bit mask: head data updated */ + public static final int CHANGE_HEAP_DATA = 0x0040; + /** Client change bit mask: native heap data updated */ + public static final int CHANGE_NATIVE_HEAP_DATA = 0x0080; + /** Client change bit mask: thread stack trace updated */ + public static final int CHANGE_THREAD_STACKTRACE = 0x0100; + /** Client change bit mask: allocation information updated */ + public static final int CHANGE_HEAP_ALLOCATIONS = 0x0200; + /** Client change bit mask: allocation information updated */ + public static final int CHANGE_HEAP_ALLOCATION_STATUS = 0x0400; + /** Client change bit mask: allocation information updated */ + public static final int CHANGE_METHOD_PROFILING_STATUS = 0x0800; + + /** Client change bit mask: combination of {@link Client#CHANGE_NAME}, + * {@link Client#CHANGE_DEBUGGER_STATUS}, and {@link Client#CHANGE_PORT}. + */ + public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_STATUS | CHANGE_PORT; + + private SocketChannel mChan; + + // debugger we're associated with, if any + private Debugger mDebugger; + private int mDebuggerListenPort; + + // list of IDs for requests we have sent to the client + private HashMap mOutstandingReqs; + + // chunk handlers stash state data in here + private ClientData mClientData; + + // User interface state. Changing the value causes a message to be + // sent to the client. + private boolean mThreadUpdateEnabled; + private boolean mHeapUpdateEnabled; + + /* + * Read/write buffers. We can get large quantities of data from the + * client, e.g. the response to a "give me the list of all known classes" + * request from the debugger. Requests from the debugger, and from us, + * are much smaller. + * + * Pass-through debugger traffic is sent without copying. "mWriteBuffer" + * is only used for data generated within Client. + */ + private static final int INITIAL_BUF_SIZE = 2*1024; + private static final int MAX_BUF_SIZE = 200*1024*1024; + private ByteBuffer mReadBuffer; + + private static final int WRITE_BUF_SIZE = 256; + private ByteBuffer mWriteBuffer; + + private Device mDevice; + + private int mConnState; + + private static final int ST_INIT = 1; + private static final int ST_NOT_JDWP = 2; + private static final int ST_AWAIT_SHAKE = 10; + private static final int ST_NEED_DDM_PKT = 11; + private static final int ST_NOT_DDM = 12; + private static final int ST_READY = 13; + private static final int ST_ERROR = 20; + private static final int ST_DISCONNECTED = 21; + + + /** + * Create an object for a new client connection. + * + * @param device the device this client belongs to + * @param chan the connected {@link SocketChannel}. + * @param pid the client pid. + */ + Client(Device device, SocketChannel chan, int pid) { + mDevice = device; + mChan = chan; + + mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE); + mWriteBuffer = ByteBuffer.allocate(WRITE_BUF_SIZE); + + mOutstandingReqs = new HashMap(); + + mConnState = ST_INIT; + + mClientData = new ClientData(pid); + + mThreadUpdateEnabled = DdmPreferences.getInitialThreadUpdate(); + mHeapUpdateEnabled = DdmPreferences.getInitialHeapUpdate(); + } + + /** + * Returns a string representation of the {@link Client} object. + */ + @Override + public String toString() { + return "[Client pid: " + mClientData.getPid() + "]"; + } + + /** + * Returns the {@link IDevice} on which this Client is running. + */ + public IDevice getDevice() { + return mDevice; + } + + /** Returns the {@link Device} on which this Client is running. + */ + Device getDeviceImpl() { + return mDevice; + } + + /** + * Returns the debugger port for this client. + */ + public int getDebuggerListenPort() { + return mDebuggerListenPort; + } + + /** + * Returns true if the client VM is DDM-aware. + * + * Calling here is only allowed after the connection has been + * established. + */ + public boolean isDdmAware() { + switch (mConnState) { + case ST_INIT: + case ST_NOT_JDWP: + case ST_AWAIT_SHAKE: + case ST_NEED_DDM_PKT: + case ST_NOT_DDM: + case ST_ERROR: + case ST_DISCONNECTED: + return false; + case ST_READY: + return true; + default: + assert false; + return false; + } + } + + /** + * Returns true if a debugger is currently attached to the client. + */ + public boolean isDebuggerAttached() { + return mDebugger.isDebuggerAttached(); + } + + /** + * Return the Debugger object associated with this client. + */ + Debugger getDebugger() { + return mDebugger; + } + + /** + * Returns the {@link ClientData} object containing this client information. + */ + public ClientData getClientData() { + return mClientData; + } + + /** + * Forces the client to execute its garbage collector. + */ + public void executeGarbageCollector() { + try { + HandleHeap.sendHPGC(this); + } catch (IOException ioe) { + Log.w("ddms", "Send of HPGC message failed"); + // ignore + } + } + + /** + * Makes the VM dump an HPROF file + */ + public void dumpHprof() { + boolean canStream = mClientData.hasFeature(ClientData.FEATURE_HPROF_STREAMING); + try { + if (canStream) { + HandleHeap.sendHPDS(this); + } else { + String file = "/sdcard/" + mClientData.getClientDescription().replaceAll( + "\\:.*", "") + ".hprof"; + HandleHeap.sendHPDU(this, file); + } + } catch (IOException e) { + Log.w("ddms", "Send of HPDU message failed"); + // ignore + } + } + + public void toggleMethodProfiling() { + boolean canStream = mClientData.hasFeature(ClientData.FEATURE_PROFILING_STREAMING); + try { + if (mClientData.getMethodProfilingStatus() == MethodProfilingStatus.ON) { + if (canStream) { + HandleProfiling.sendMPSE(this); + } else { + HandleProfiling.sendMPRE(this); + } + } else { + if (canStream) { + HandleProfiling.sendMPSS(this, 8*1024*1024, 0 /*flags*/); + } else { + String file = "/sdcard/" + mClientData.getClientDescription().replaceAll("\\:.*", "") + + ".trace"; + HandleProfiling.sendMPRS(this, file, 8*1024*1024, 0 /*flags*/); + } + } + } catch (IOException e) { + Log.w("ddms", "Toggle method profiling failed"); + // ignore + } + } + + /** + * Sends a request to the VM to send the enable status of the method profiling. + * This is asynchronous. + *

The allocation status can be accessed by {@link ClientData#getAllocationStatus()}. + * The notification that the new status is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}. + */ + public void requestMethodProfilingStatus() { + try { + HandleHeap.sendREAQ(this); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + + /** + * Enables or disables the thread update. + *

If true the VM will be able to send thread information. Thread information + * must be requested with {@link #requestThreadUpdate()}. + * @param enabled the enable flag. + */ + public void setThreadUpdateEnabled(boolean enabled) { + mThreadUpdateEnabled = enabled; + if (enabled == false) { + mClientData.clearThreads(); + } + + try { + HandleThread.sendTHEN(this, enabled); + } catch (IOException ioe) { + // ignore it here; client will clean up shortly + ioe.printStackTrace(); + } + + update(CHANGE_THREAD_MODE); + } + + /** + * Returns whether the thread update is enabled. + */ + public boolean isThreadUpdateEnabled() { + return mThreadUpdateEnabled; + } + + /** + * Sends a thread update request. This is asynchronous. + *

The thread info can be accessed by {@link ClientData#getThreads()}. The notification + * that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_THREAD_DATA}. + */ + public void requestThreadUpdate() { + HandleThread.requestThreadUpdate(this); + } + + /** + * Sends a thread stack trace update request. This is asynchronous. + *

The thread info can be accessed by {@link ClientData#getThreads()} and + * {@link ThreadInfo#getStackTrace()}. + *

The notification that the new data is available + * will be received through {@link IClientChangeListener#clientChanged(Client, int)} + * with a changeMask containing the mask {@link #CHANGE_THREAD_STACKTRACE}. + */ + public void requestThreadStackTrace(int threadId) { + HandleThread.requestThreadStackCallRefresh(this, threadId); + } + + /** + * Enables or disables the heap update. + *

If true, any GC will cause the client to send its heap information. + *

The heap information can be accessed by {@link ClientData#getVmHeapData()}. + *

The notification that the new data is available + * will be received through {@link IClientChangeListener#clientChanged(Client, int)} + * with a changeMask containing the value {@link #CHANGE_HEAP_DATA}. + * @param enabled the enable flag + */ + public void setHeapUpdateEnabled(boolean enabled) { + mHeapUpdateEnabled = enabled; + + try { + HandleHeap.sendHPIF(this, + enabled ? HandleHeap.HPIF_WHEN_EVERY_GC : HandleHeap.HPIF_WHEN_NEVER); + + HandleHeap.sendHPSG(this, + enabled ? HandleHeap.WHEN_GC : HandleHeap.WHEN_DISABLE, + HandleHeap.WHAT_MERGE); + } catch (IOException ioe) { + // ignore it here; client will clean up shortly + } + + update(CHANGE_HEAP_MODE); + } + + /** + * Returns whether the heap update is enabled. + * @see #setHeapUpdateEnabled(boolean) + */ + public boolean isHeapUpdateEnabled() { + return mHeapUpdateEnabled; + } + + /** + * Sends a native heap update request. this is asynchronous. + *

The native heap info can be accessed by {@link ClientData#getNativeAllocationList()}. + * The notification that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_NATIVE_HEAP_DATA}. + */ + public boolean requestNativeHeapInformation() { + try { + HandleNativeHeap.sendNHGT(this); + return true; + } catch (IOException e) { + Log.e("ddmlib", e); + } + + return false; + } + + /** + * Enables or disables the Allocation tracker for this client. + *

If enabled, the VM will start tracking allocation informations. A call to + * {@link #requestAllocationDetails()} will make the VM sends the information about all the + * allocations that happened between the enabling and the request. + * @param enable + * @see #requestAllocationDetails() + */ + public void enableAllocationTracker(boolean enable) { + try { + HandleHeap.sendREAE(this, enable); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a request to the VM to send the enable status of the allocation tracking. + * This is asynchronous. + *

The allocation status can be accessed by {@link ClientData#getAllocationStatus()}. + * The notification that the new status is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}. + */ + public void requestAllocationStatus() { + try { + HandleHeap.sendREAQ(this); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a request to the VM to send the information about all the allocations that have + * happened since the call to {@link #enableAllocationTracker(boolean)} with enable + * set to null. This is asynchronous. + *

The allocation information can be accessed by {@link ClientData#getAllocations()}. + * The notification that the new data is available will be received through + * {@link IClientChangeListener#clientChanged(Client, int)} with a changeMask + * containing the mask {@link #CHANGE_HEAP_ALLOCATIONS}. + */ + public void requestAllocationDetails() { + try { + HandleHeap.sendREAL(this); + } catch (IOException e) { + Log.e("ddmlib", e); + } + } + + /** + * Sends a kill message to the VM. + */ + public void kill() { + try { + HandleExit.sendEXIT(this, 1); + } catch (IOException ioe) { + Log.w("ddms", "Send of EXIT message failed"); + // ignore + } + } + + /** + * Registers the client with a Selector. + */ + void register(Selector sel) throws IOException { + if (mChan != null) { + mChan.register(sel, SelectionKey.OP_READ, this); + } + } + + /** + * Sets the client to accept debugger connection on the "selected debugger port". + * + * @see AndroidDebugBridge#setSelectedClient(Client) + * @see DdmPreferences#setSelectedDebugPort(int) + */ + public void setAsSelectedClient() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setSelectedClient(this); + } + } + + /** + * Returns whether this client is the current selected client, accepting debugger connection + * on the "selected debugger port". + * + * @see #setAsSelectedClient() + * @see AndroidDebugBridge#setSelectedClient(Client) + * @see DdmPreferences#setSelectedDebugPort(int) + */ + public boolean isSelectedClient() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + return monitorThread.getSelectedClient() == this; + } + + return false; + } + + /** + * Tell the client to open a server socket channel and listen for + * connections on the specified port. + */ + void listenForDebugger(int listenPort) throws IOException { + mDebuggerListenPort = listenPort; + mDebugger = new Debugger(this, listenPort); + } + + /** + * Initiate the JDWP handshake. + * + * On failure, closes the socket and returns false. + */ + boolean sendHandshake() { + assert mWriteBuffer.position() == 0; + + try { + // assume write buffer can hold 14 bytes + JdwpPacket.putHandshake(mWriteBuffer); + int expectedLen = mWriteBuffer.position(); + mWriteBuffer.flip(); + if (mChan.write(mWriteBuffer) != expectedLen) + throw new IOException("partial handshake write"); + } + catch (IOException ioe) { + Log.e("ddms-client", "IO error during handshake: " + ioe.getMessage()); + mConnState = ST_ERROR; + close(true /* notify */); + return false; + } + finally { + mWriteBuffer.clear(); + } + + mConnState = ST_AWAIT_SHAKE; + + return true; + } + + + /** + * Send a non-DDM packet to the client. + * + * Equivalent to sendAndConsume(packet, null). + */ + void sendAndConsume(JdwpPacket packet) throws IOException { + sendAndConsume(packet, null); + } + + /** + * Send a DDM packet to the client. + * + * Ideally, we can do this with a single channel write. If that doesn't + * happen, we have to prevent anybody else from writing to the channel + * until this packet completes, so we synchronize on the channel. + * + * Another goal is to avoid unnecessary buffer copies, so we write + * directly out of the JdwpPacket's ByteBuffer. + */ + void sendAndConsume(JdwpPacket packet, ChunkHandler replyHandler) + throws IOException { + + if (mChan == null) { + // can happen for e.g. THST packets + Log.v("ddms", "Not sending packet -- client is closed"); + return; + } + + if (replyHandler != null) { + /* + * Add the ID to the list of outstanding requests. We have to do + * this before sending the packet, in case the response comes back + * before our thread returns from the packet-send function. + */ + addRequestId(packet.getId(), replyHandler); + } + + synchronized (mChan) { + try { + packet.writeAndConsume(mChan); + } + catch (IOException ioe) { + removeRequestId(packet.getId()); + throw ioe; + } + } + } + + /** + * Forward the packet to the debugger (if still connected to one). + * + * Consumes the packet. + */ + void forwardPacketToDebugger(JdwpPacket packet) + throws IOException { + + Debugger dbg = mDebugger; + + if (dbg == null) { + Log.d("ddms", "Discarding packet"); + packet.consume(); + } else { + dbg.sendAndConsume(packet); + } + } + + /** + * Read data from our channel. + * + * This is called when data is known to be available, and we don't yet + * have a full packet in the buffer. If the buffer is at capacity, + * expand it. + */ + void read() + throws IOException, BufferOverflowException { + + int count; + + if (mReadBuffer.position() == mReadBuffer.capacity()) { + if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) { + Log.e("ddms", "Exceeded MAX_BUF_SIZE!"); + throw new BufferOverflowException(); + } + Log.d("ddms", "Expanding read buffer to " + + mReadBuffer.capacity() * 2); + + ByteBuffer newBuffer = ByteBuffer.allocate(mReadBuffer.capacity() * 2); + + // copy entire buffer to new buffer + mReadBuffer.position(0); + newBuffer.put(mReadBuffer); // leaves "position" at end of copied + + mReadBuffer = newBuffer; + } + + count = mChan.read(mReadBuffer); + if (count < 0) + throw new IOException("read failed"); + + if (Log.Config.LOGV) Log.v("ddms", "Read " + count + " bytes from " + this); + //Log.hexDump("ddms", Log.DEBUG, mReadBuffer.array(), + // mReadBuffer.arrayOffset(), mReadBuffer.position()); + } + + /** + * Return information for the first full JDWP packet in the buffer. + * + * If we don't yet have a full packet, return null. + * + * If we haven't yet received the JDWP handshake, we watch for it here + * and consume it without admitting to have done so. Upon receipt + * we send out the "HELO" message, which is why this can throw an + * IOException. + */ + JdwpPacket getJdwpPacket() throws IOException { + + /* + * On entry, the data starts at offset 0 and ends at "position". + * "limit" is set to the buffer capacity. + */ + if (mConnState == ST_AWAIT_SHAKE) { + /* + * The first thing we get from the client is a response to our + * handshake. It doesn't look like a packet, so we have to + * handle it specially. + */ + int result; + + result = JdwpPacket.findHandshake(mReadBuffer); + //Log.v("ddms", "findHand: " + result); + switch (result) { + case JdwpPacket.HANDSHAKE_GOOD: + Log.d("ddms", + "Good handshake from client, sending HELO to " + mClientData.getPid()); + JdwpPacket.consumeHandshake(mReadBuffer); + mConnState = ST_NEED_DDM_PKT; + HandleHello.sendHelloCommands(this, SERVER_PROTOCOL_VERSION); + // see if we have another packet in the buffer + return getJdwpPacket(); + case JdwpPacket.HANDSHAKE_BAD: + Log.d("ddms", "Bad handshake from client"); + if (MonitorThread.getInstance().getRetryOnBadHandshake()) { + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + mDevice.getMonitor().addClientToDropAndReopen(this, + IDebugPortProvider.NO_STATIC_PORT); + } else { + // mark it as bad, close the socket, and don't retry + mConnState = ST_NOT_JDWP; + close(true /* notify */); + } + break; + case JdwpPacket.HANDSHAKE_NOTYET: + Log.d("ddms", "No handshake from client yet."); + break; + default: + Log.e("ddms", "Unknown packet while waiting for client handshake"); + } + return null; + } else if (mConnState == ST_NEED_DDM_PKT || + mConnState == ST_NOT_DDM || + mConnState == ST_READY) { + /* + * Normal packet traffic. + */ + if (mReadBuffer.position() != 0) { + if (Log.Config.LOGV) Log.v("ddms", + "Checking " + mReadBuffer.position() + " bytes"); + } + return JdwpPacket.findPacket(mReadBuffer); + } else { + /* + * Not expecting data when in this state. + */ + Log.e("ddms", "Receiving data in state = " + mConnState); + } + + return null; + } + + /* + * Add the specified ID to the list of request IDs for which we await + * a response. + */ + private void addRequestId(int id, ChunkHandler handler) { + synchronized (mOutstandingReqs) { + if (Log.Config.LOGV) Log.v("ddms", + "Adding req 0x" + Integer.toHexString(id) +" to set"); + mOutstandingReqs.put(id, handler); + } + } + + /* + * Remove the specified ID from the list, if present. + */ + void removeRequestId(int id) { + synchronized (mOutstandingReqs) { + if (Log.Config.LOGV) Log.v("ddms", + "Removing req 0x" + Integer.toHexString(id) + " from set"); + mOutstandingReqs.remove(id); + } + + //Log.w("ddms", "Request " + Integer.toHexString(id) + // + " could not be removed from " + this); + } + + /** + * Determine whether this is a response to a request we sent earlier. + * If so, return the ChunkHandler responsible. + */ + ChunkHandler isResponseToUs(int id) { + + synchronized (mOutstandingReqs) { + ChunkHandler handler = mOutstandingReqs.get(id); + if (handler != null) { + if (Log.Config.LOGV) Log.v("ddms", + "Found 0x" + Integer.toHexString(id) + + " in request set - " + handler); + return handler; + } + } + + return null; + } + + /** + * An earlier request resulted in a failure. This is the expected + * response to a HELO message when talking to a non-DDM client. + */ + void packetFailed(JdwpPacket reply) { + if (mConnState == ST_NEED_DDM_PKT) { + Log.d("ddms", "Marking " + this + " as non-DDM client"); + mConnState = ST_NOT_DDM; + } else if (mConnState != ST_NOT_DDM) { + Log.w("ddms", "WEIRD: got JDWP failure packet on DDM req"); + } + } + + /** + * The MonitorThread calls this when it sees a DDM request or reply. + * If we haven't seen a DDM packet before, we advance the state to + * ST_READY and return "false". Otherwise, just return true. + * + * The idea is to let the MonitorThread know when we first see a DDM + * packet, so we can send a broadcast to the handlers when a client + * connection is made. This method is synchronized so that we only + * send the broadcast once. + */ + synchronized boolean ddmSeen() { + if (mConnState == ST_NEED_DDM_PKT) { + mConnState = ST_READY; + return false; + } else if (mConnState != ST_READY) { + Log.w("ddms", "WEIRD: in ddmSeen with state=" + mConnState); + } + return true; + } + + /** + * Close the client socket channel. If there is a debugger associated + * with us, close that too. + * + * Closing a channel automatically unregisters it from the selector. + * However, we have to iterate through the selector loop before it + * actually lets them go and allows the file descriptors to close. + * The caller is expected to manage that. + * @param notify Whether or not to notify the listeners of a change. + */ + void close(boolean notify) { + Log.d("ddms", "Closing " + this.toString()); + + mOutstandingReqs.clear(); + + try { + if (mChan != null) { + mChan.close(); + mChan = null; + } + + if (mDebugger != null) { + mDebugger.close(); + mDebugger = null; + } + } + catch (IOException ioe) { + Log.w("ddms", "failed to close " + this); + // swallow it -- not much else to do + } + + mDevice.removeClient(this, notify); + } + + /** + * Returns whether this {@link Client} has a valid connection to the application VM. + */ + public boolean isValid() { + return mChan != null; + } + + void update(int changeMask) { + mDevice.update(this, changeMask); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java new file mode 100644 index 000000000..7f4b5dd0e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java @@ -0,0 +1,694 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; + + +/** + * Contains the data of a {@link Client}. + */ +public class ClientData { + /* This is a place to stash data associated with a Client, such as thread + * states or heap data. ClientData maps 1:1 to Client, but it's a little + * cleaner if we separate the data out. + * + * Message handlers are welcome to stash arbitrary data here. + * + * IMPORTANT: The data here is written by HandleFoo methods and read by + * FooPanel methods, which run in different threads. All non-trivial + * access should be synchronized against the ClientData object. + */ + + + /** Temporary name of VM to be ignored. */ + private final static String PRE_INITIALIZED = ""; //$NON-NLS-1$ + + public static enum DebuggerStatus { + /** Debugger connection status: not waiting on one, not connected to one, but accepting + * new connections. This is the default value. */ + DEFAULT, + /** + * Debugger connection status: the application's VM is paused, waiting for a debugger to + * connect to it before resuming. */ + WAITING, + /** Debugger connection status : Debugger is connected */ + ATTACHED, + /** Debugger connection status: The listening port for debugger connection failed to listen. + * No debugger will be able to connect. */ + ERROR; + } + + public static enum AllocationTrackingStatus { + /** + * Allocation tracking status: unknown. + *

This happens right after a {@link Client} is discovered + * by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query + * regarding its allocation tracking status. + * @see Client#requestAllocationStatus() + */ + UNKNOWN, + /** Allocation tracking status: the {@link Client} is not tracking allocations. */ + OFF, + /** Allocation tracking status: the {@link Client} is tracking allocations. */ + ON; + } + + public static enum MethodProfilingStatus { + /** + * Method profiling status: unknown. + *

This happens right after a {@link Client} is discovered + * by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query + * regarding its method profiling status. + * @see Client#requestMethodProfilingStatus() + */ + UNKNOWN, + /** Method profiling status: the {@link Client} is not profiling method calls. */ + OFF, + /** Method profiling status: the {@link Client} is profiling method calls. */ + ON; + } + + /** + * Name of the value representing the max size of the heap, in the {@link Map} returned by + * {@link #getVmHeapInfo(int)} + */ + public final static String HEAP_MAX_SIZE_BYTES = "maxSizeInBytes"; // $NON-NLS-1$ + /** + * Name of the value representing the size of the heap, in the {@link Map} returned by + * {@link #getVmHeapInfo(int)} + */ + public final static String HEAP_SIZE_BYTES = "sizeInBytes"; // $NON-NLS-1$ + /** + * Name of the value representing the number of allocated bytes of the heap, in the + * {@link Map} returned by {@link #getVmHeapInfo(int)} + */ + public final static String HEAP_BYTES_ALLOCATED = "bytesAllocated"; // $NON-NLS-1$ + /** + * Name of the value representing the number of objects in the heap, in the {@link Map} + * returned by {@link #getVmHeapInfo(int)} + */ + public final static String HEAP_OBJECTS_ALLOCATED = "objectsAllocated"; // $NON-NLS-1$ + + /** + * String for feature enabling starting/stopping method profiling + * @see #hasFeature(String) + */ + public final static String FEATURE_PROFILING = "method-trace-profiling"; // $NON-NLS-1$ + + /** + * String for feature enabling direct streaming of method profiling data + * @see #hasFeature(String) + */ + public final static String FEATURE_PROFILING_STREAMING = "method-trace-profiling-streaming"; // $NON-NLS-1$ + + /** + * String for feature allowing to dump hprof files + * @see #hasFeature(String) + */ + public final static String FEATURE_HPROF = "hprof-heap-dump"; // $NON-NLS-1$ + + /** + * String for feature allowing direct streaming of hprof dumps + * @see #hasFeature(String) + */ + public final static String FEATURE_HPROF_STREAMING = "hprof-heap-dump-streaming"; // $NON-NLS-1$ + + private static IHprofDumpHandler sHprofDumpHandler; + private static IMethodProfilingHandler sMethodProfilingHandler; + + // is this a DDM-aware client? + private boolean mIsDdmAware; + + // the client's process ID + private final int mPid; + + // Java VM identification string + private String mVmIdentifier; + + // client's self-description + private String mClientDescription; + + // how interested are we in a debugger? + private DebuggerStatus mDebuggerInterest; + + // List of supported features by the client. + private final HashSet mFeatures = new HashSet(); + + // Thread tracking (THCR, THDE). + private TreeMap mThreadMap; + + /** VM Heap data */ + private final HeapData mHeapData = new HeapData(); + /** Native Heap data */ + private final HeapData mNativeHeapData = new HeapData(); + + private HashMap> mHeapInfoMap = + new HashMap>(); + + + /** library map info. Stored here since the backtrace data + * is computed on a need to display basis. + */ + private ArrayList mNativeLibMapInfo = + new ArrayList(); + + /** Native Alloc info list */ + private ArrayList mNativeAllocationList = + new ArrayList(); + private int mNativeTotalMemory; + + private AllocationInfo[] mAllocations; + private AllocationTrackingStatus mAllocationStatus = AllocationTrackingStatus.UNKNOWN; + + private String mPendingHprofDump; + + private MethodProfilingStatus mProfilingStatus = MethodProfilingStatus.UNKNOWN; + private String mPendingMethodProfiling; + + /** + * Heap Information. + *

The heap is composed of several {@link HeapSegment} objects. + *

A call to {@link #isHeapDataComplete()} will indicate if the segments (available through + * {@link #getHeapSegments()}) represent the full heap. + */ + public static class HeapData { + private TreeSet mHeapSegments = new TreeSet(); + private boolean mHeapDataComplete = false; + private byte[] mProcessedHeapData; + private Map> mProcessedHeapMap; + + /** + * Abandon the current list of heap segments. + */ + public synchronized void clearHeapData() { + /* Abandon the old segments instead of just calling .clear(). + * This lets the user hold onto the old set if it wants to. + */ + mHeapSegments = new TreeSet(); + mHeapDataComplete = false; + } + + /** + * Add raw HPSG chunk data to the list of heap segments. + * + * @param data The raw data from an HPSG chunk. + */ + synchronized void addHeapData(ByteBuffer data) { + HeapSegment hs; + + if (mHeapDataComplete) { + clearHeapData(); + } + + try { + hs = new HeapSegment(data); + } catch (BufferUnderflowException e) { + System.err.println("Discarding short HPSG data (length " + data.limit() + ")"); + return; + } + + mHeapSegments.add(hs); + } + + /** + * Called when all heap data has arrived. + */ + synchronized void sealHeapData() { + mHeapDataComplete = true; + } + + /** + * Returns whether the heap data has been sealed. + */ + public boolean isHeapDataComplete() { + return mHeapDataComplete; + } + + /** + * Get the collected heap data, if sealed. + * + * @return The list of heap segments if the heap data has been sealed, or null if it hasn't. + */ + public Collection getHeapSegments() { + if (isHeapDataComplete()) { + return mHeapSegments; + } + return null; + } + + /** + * Sets the processed heap data. + * + * @param heapData The new heap data (can be null) + */ + public void setProcessedHeapData(byte[] heapData) { + mProcessedHeapData = heapData; + } + + /** + * Get the processed heap data, if present. + * + * @return the processed heap data, or null. + */ + public byte[] getProcessedHeapData() { + return mProcessedHeapData; + } + + public void setProcessedHeapMap(Map> heapMap) { + mProcessedHeapMap = heapMap; + } + + public Map> getProcessedHeapMap() { + return mProcessedHeapMap; + } + } + + /** + * Handlers able to act on HPROF dumps. + */ + public interface IHprofDumpHandler { + /** + * Called when a HPROF dump succeeded. + * @param remoteFilePath the device-side path of the HPROF file. + * @param client the client for which the HPROF file was. + */ + void onSuccess(String remoteFilePath, Client client); + + /** + * Called when a HPROF dump was successful. + * @param data the data containing the HPROF file, streamed from the VM + * @param client the client that was profiled. + */ + void onSuccess(byte[] data, Client client); + + /** + * Called when a hprof dump failed to end on the VM side + * @param client the client that was profiled. + * @param message an optional (null ok) error message to be displayed. + */ + void onEndFailure(Client client, String message); + } + + /** + * Handlers able to act on Method profiling info + */ + public interface IMethodProfilingHandler { + /** + * Called when a method tracing was successful. + * @param remoteFilePath the device-side path of the trace file. + * @param client the client that was profiled. + */ + void onSuccess(String remoteFilePath, Client client); + + /** + * Called when a method tracing was successful. + * @param data the data containing the trace file, streamed from the VM + * @param client the client that was profiled. + */ + void onSuccess(byte[] data, Client client); + + /** + * Called when method tracing failed to start + * @param client the client that was profiled. + * @param message an optional (null ok) error message to be displayed. + */ + void onStartFailure(Client client, String message); + + /** + * Called when method tracing failed to end on the VM side + * @param client the client that was profiled. + * @param message an optional (null ok) error message to be displayed. + */ + void onEndFailure(Client client, String message); + } + + /** + * Sets the handler to receive notifications when an HPROF dump succeeded or failed. + */ + public static void setHprofDumpHandler(IHprofDumpHandler handler) { + sHprofDumpHandler = handler; + } + + static IHprofDumpHandler getHprofDumpHandler() { + return sHprofDumpHandler; + } + + /** + * Sets the handler to receive notifications when an HPROF dump succeeded or failed. + */ + public static void setMethodProfilingHandler(IMethodProfilingHandler handler) { + sMethodProfilingHandler = handler; + } + + static IMethodProfilingHandler getMethodProfilingHandler() { + return sMethodProfilingHandler; + } + + /** + * Generic constructor. + */ + ClientData(int pid) { + mPid = pid; + + mDebuggerInterest = DebuggerStatus.DEFAULT; + mThreadMap = new TreeMap(); + } + + /** + * Returns whether the process is DDM-aware. + */ + public boolean isDdmAware() { + return mIsDdmAware; + } + + /** + * Sets DDM-aware status. + */ + void isDdmAware(boolean aware) { + mIsDdmAware = aware; + } + + /** + * Returns the process ID. + */ + public int getPid() { + return mPid; + } + + /** + * Returns the Client's VM identifier. + */ + public String getVmIdentifier() { + return mVmIdentifier; + } + + /** + * Sets VM identifier. + */ + void setVmIdentifier(String ident) { + mVmIdentifier = ident; + } + + /** + * Returns the client description. + *

This is generally the name of the package defined in the + * AndroidManifest.xml. + * + * @return the client description or null if not the description was not yet + * sent by the client. + */ + public String getClientDescription() { + return mClientDescription; + } + + /** + * Sets client description. + * + * There may be a race between HELO and APNM. Rather than try + * to enforce ordering on the device, we just don't allow an empty + * name to replace a specified one. + */ + void setClientDescription(String description) { + if (mClientDescription == null && description.length() > 0) { + /* + * The application VM is first named before being assigned + * its real name. + * Depending on the timing, we can get an APNM chunk setting this name before + * another one setting the final actual name. So if we get a SetClientDescription + * with this value we ignore it. + */ + if (PRE_INITIALIZED.equals(description) == false) { + mClientDescription = description; + } + } + } + + /** + * Returns the debugger connection status. + */ + public DebuggerStatus getDebuggerConnectionStatus() { + return mDebuggerInterest; + } + + /** + * Sets debugger connection status. + */ + void setDebuggerConnectionStatus(DebuggerStatus status) { + mDebuggerInterest = status; + } + + /** + * Sets the current heap info values for the specified heap. + * + * @param heapId The heap whose info to update + * @param sizeInBytes The size of the heap, in bytes + * @param bytesAllocated The number of bytes currently allocated in the heap + * @param objectsAllocated The number of objects currently allocated in + * the heap + */ + // TODO: keep track of timestamp, reason + synchronized void setHeapInfo(int heapId, long maxSizeInBytes, + long sizeInBytes, long bytesAllocated, long objectsAllocated) { + HashMap heapInfo = new HashMap(); + heapInfo.put(HEAP_MAX_SIZE_BYTES, maxSizeInBytes); + heapInfo.put(HEAP_SIZE_BYTES, sizeInBytes); + heapInfo.put(HEAP_BYTES_ALLOCATED, bytesAllocated); + heapInfo.put(HEAP_OBJECTS_ALLOCATED, objectsAllocated); + mHeapInfoMap.put(heapId, heapInfo); + } + + /** + * Returns the {@link HeapData} object for the VM. + */ + public HeapData getVmHeapData() { + return mHeapData; + } + + /** + * Returns the {@link HeapData} object for the native code. + */ + HeapData getNativeHeapData() { + return mNativeHeapData; + } + + /** + * Returns an iterator over the list of known VM heap ids. + *

+ * The caller must synchronize on the {@link ClientData} object while iterating. + * + * @return an iterator over the list of heap ids + */ + public synchronized Iterator getVmHeapIds() { + return mHeapInfoMap.keySet().iterator(); + } + + /** + * Returns the most-recent info values for the specified VM heap. + * + * @param heapId The heap whose info should be returned + * @return a map containing the info values for the specified heap. + * Returns null if the heap ID is unknown. + */ + public synchronized Map getVmHeapInfo(int heapId) { + return mHeapInfoMap.get(heapId); + } + + /** + * Adds a new thread to the list. + */ + synchronized void addThread(int threadId, String threadName) { + ThreadInfo attr = new ThreadInfo(threadId, threadName); + mThreadMap.put(threadId, attr); + } + + /** + * Removes a thread from the list. + */ + synchronized void removeThread(int threadId) { + mThreadMap.remove(threadId); + } + + /** + * Returns the list of threads as {@link ThreadInfo} objects. + *

The list is empty until a thread update was requested with + * {@link Client#requestThreadUpdate()}. + */ + public synchronized ThreadInfo[] getThreads() { + Collection threads = mThreadMap.values(); + return threads.toArray(new ThreadInfo[threads.size()]); + } + + /** + * Returns the {@link ThreadInfo} by thread id. + */ + synchronized ThreadInfo getThread(int threadId) { + return mThreadMap.get(threadId); + } + + synchronized void clearThreads() { + mThreadMap.clear(); + } + + /** + * Returns the list of {@link NativeAllocationInfo}. + * @see Client#requestNativeHeapInformation() + */ + public synchronized List getNativeAllocationList() { + return Collections.unmodifiableList(mNativeAllocationList); + } + + /** + * adds a new {@link NativeAllocationInfo} to the {@link Client} + * @param allocInfo The {@link NativeAllocationInfo} to add. + */ + synchronized void addNativeAllocation(NativeAllocationInfo allocInfo) { + mNativeAllocationList.add(allocInfo); + } + + /** + * Clear the current malloc info. + */ + synchronized void clearNativeAllocationInfo() { + mNativeAllocationList.clear(); + } + + /** + * Returns the total native memory. + * @see Client#requestNativeHeapInformation() + */ + public synchronized int getTotalNativeMemory() { + return mNativeTotalMemory; + } + + synchronized void setTotalNativeMemory(int totalMemory) { + mNativeTotalMemory = totalMemory; + } + + synchronized void addNativeLibraryMapInfo(long startAddr, long endAddr, String library) { + mNativeLibMapInfo.add(new NativeLibraryMapInfo(startAddr, endAddr, library)); + } + + /** + * Returns an {@link Iterator} on {@link NativeLibraryMapInfo} objects. + *

+ * The caller must synchronize on the {@link ClientData} object while iterating. + */ + public synchronized Iterator getNativeLibraryMapInfo() { + return mNativeLibMapInfo.iterator(); + } + + synchronized void setAllocationStatus(AllocationTrackingStatus status) { + mAllocationStatus = status; + } + + /** + * Returns the allocation tracking status. + * @see Client#requestAllocationStatus() + */ + public synchronized AllocationTrackingStatus getAllocationStatus() { + return mAllocationStatus; + } + + synchronized void setAllocations(AllocationInfo[] allocs) { + mAllocations = allocs; + } + + /** + * Returns the list of tracked allocations. + * @see Client#requestAllocationDetails() + */ + public synchronized AllocationInfo[] getAllocations() { + return mAllocations; + } + + void addFeature(String feature) { + mFeatures.add(feature); + } + + /** + * Returns true if the {@link Client} supports the given feature + * @param feature The feature to test. + * @return true if the feature is supported + * + * @see ClientData#FEATURE_PROFILING + * @see ClientData#FEATURE_HPROF + */ + public boolean hasFeature(String feature) { + return mFeatures.contains(feature); + } + + /** + * Sets the device-side path to the hprof file being written + * @param pendingHprofDump the file to the hprof file + */ + void setPendingHprofDump(String pendingHprofDump) { + mPendingHprofDump = pendingHprofDump; + } + + /** + * Returns the path to the device-side hprof file being written. + */ + String getPendingHprofDump() { + return mPendingHprofDump; + } + + public boolean hasPendingHprofDump() { + return mPendingHprofDump != null; + } + + synchronized void setMethodProfilingStatus(MethodProfilingStatus status) { + mProfilingStatus = status; + } + + /** + * Returns the method profiling status. + * @see Client#requestMethodProfilingStatus() + */ + public synchronized MethodProfilingStatus getMethodProfilingStatus() { + return mProfilingStatus; + } + + /** + * Sets the device-side path to the method profile file being written + * @param pendingMethodProfiling the file being written + */ + void setPendingMethodProfiling(String pendingMethodProfiling) { + mPendingMethodProfiling = pendingMethodProfiling; + } + + /** + * Returns the path to the device-side method profiling file being written. + */ + String getPendingMethodProfiling() { + return mPendingMethodProfiling; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DdmConstants.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmConstants.java new file mode 100644 index 000000000..d9823f3a7 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmConstants.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009 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.ddmlib; + +public final class DdmConstants { + + public final static int PLATFORM_UNKNOWN = 0; + public final static int PLATFORM_LINUX = 1; + public final static int PLATFORM_WINDOWS = 2; + public final static int PLATFORM_DARWIN = 3; + + /** + * Returns current platform, one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, + * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. + */ + public final static int CURRENT_PLATFORM = currentPlatform(); + + /** hprof-conv executable (with extension for the current OS) */ + public final static String FN_HPROF_CONVERTER = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "hprof-conv.exe" : "hprof-conv"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** traceview executable (with extension for the current OS) */ + public final static String FN_TRACEVIEW = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "traceview.bat" : "traceview"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** + * Returns current platform + * + * @return one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, + * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. + */ + public static int currentPlatform() { + String os = System.getProperty("os.name"); //$NON-NLS-1$ + if (os.startsWith("Mac OS")) { //$NON-NLS-1$ + return PLATFORM_DARWIN; + } else if (os.startsWith("Windows")) { //$NON-NLS-1$ + return PLATFORM_WINDOWS; + } else if (os.startsWith("Linux")) { //$NON-NLS-1$ + return PLATFORM_LINUX; + } + + return PLATFORM_UNKNOWN; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java new file mode 100644 index 000000000..8044ab11b --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.Log.LogLevel; + +/** + * Preferences for the ddm library. + *

This class does not handle storing the preferences. It is merely a central point for + * applications using the ddmlib to override the default values. + *

Various components of the ddmlib query this class to get their values. + *

Calls to some set##() methods will update the components using the values + * right away, while other methods will have no effect once {@link AndroidDebugBridge#init(boolean)} + * has been called. + *

Check the documentation of each method. + */ +public final class DdmPreferences { + + /** Default value for thread update flag upon client connection. */ + public final static boolean DEFAULT_INITIAL_THREAD_UPDATE = false; + /** Default value for heap update flag upon client connection. */ + public final static boolean DEFAULT_INITIAL_HEAP_UPDATE = false; + /** Default value for the selected client debug port */ + public final static int DEFAULT_SELECTED_DEBUG_PORT = 8700; + /** Default value for the debug port base */ + public final static int DEFAULT_DEBUG_PORT_BASE = 8600; + /** Default value for the logcat {@link LogLevel} */ + public final static LogLevel DEFAULT_LOG_LEVEL = LogLevel.ERROR; + /** Default timeout values for adb connection (milliseconds) */ + public static final int DEFAULT_TIMEOUT = 5000; // standard delay, in ms + + private static boolean sThreadUpdate = DEFAULT_INITIAL_THREAD_UPDATE; + private static boolean sInitialHeapUpdate = DEFAULT_INITIAL_HEAP_UPDATE; + + private static int sSelectedDebugPort = DEFAULT_SELECTED_DEBUG_PORT; + private static int sDebugPortBase = DEFAULT_DEBUG_PORT_BASE; + private static LogLevel sLogLevel = DEFAULT_LOG_LEVEL; + private static int sTimeOut = DEFAULT_TIMEOUT; + + /** + * Returns the initial {@link Client} flag for thread updates. + * @see #setInitialThreadUpdate(boolean) + */ + public static boolean getInitialThreadUpdate() { + return sThreadUpdate; + } + + /** + * Sets the initial {@link Client} flag for thread updates. + *

This change takes effect right away, for newly created {@link Client} objects. + */ + public static void setInitialThreadUpdate(boolean state) { + sThreadUpdate = state; + } + + /** + * Returns the initial {@link Client} flag for heap updates. + * @see #setInitialHeapUpdate(boolean) + */ + public static boolean getInitialHeapUpdate() { + return sInitialHeapUpdate; + } + + /** + * Sets the initial {@link Client} flag for heap updates. + *

If true, the {@link ClientData} will automatically be updated with + * the VM heap information whenever a GC happens. + *

This change takes effect right away, for newly created {@link Client} objects. + */ + public static void setInitialHeapUpdate(boolean state) { + sInitialHeapUpdate = state; + } + + /** + * Returns the debug port used by the selected {@link Client}. + */ + public static int getSelectedDebugPort() { + return sSelectedDebugPort; + } + + /** + * Sets the debug port used by the selected {@link Client}. + *

This change takes effect right away. + * @param port the new port to use. + */ + public static void setSelectedDebugPort(int port) { + sSelectedDebugPort = port; + + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setDebugSelectedPort(port); + } + } + + /** + * Returns the debug port used by the first {@link Client}. Following clients, will use the + * next port. + */ + public static int getDebugPortBase() { + return sDebugPortBase; + } + + /** + * Sets the debug port used by the first {@link Client}. + *

Once a port is used, the next Client will use port + 1. Quitting applications will + * release their debug port, and new clients will be able to reuse them. + *

This must be called before {@link AndroidDebugBridge#init(boolean)}. + */ + public static void setDebugPortBase(int port) { + sDebugPortBase = port; + } + + /** + * Returns the minimum {@link LogLevel} being displayed. + */ + public static LogLevel getLogLevel() { + return sLogLevel; + } + + /** + * Sets the minimum {@link LogLevel} to display. + *

This change takes effect right away. + */ + public static void setLogLevel(String value) { + sLogLevel = LogLevel.getByString(value); + + Log.setLevel(sLogLevel); + } + + /** + * Returns the timeout to be used in adb connections (milliseconds). + */ + public static int getTimeOut() { + return sTimeOut; + } + + /** + * Sets the timeout value for adb connection. + *

This change takes effect for newly created connections only. + * @param timeOut the timeout value (milliseconds). + */ + public static void setTimeOut(int timeOut) { + sTimeOut = timeOut; + } + + /** + * Non accessible constructor. + */ + private DdmPreferences() { + // pass, only static methods in the class. + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java new file mode 100644 index 000000000..defdc0e1f --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.Device; + +/** + * Centralized point to provide a {@link IDebugPortProvider} to ddmlib. + * + *

When {@link Client} objects are created, they start listening for debuggers on a specific + * port. The default behavior is to start with {@link DdmPreferences#getDebugPortBase()} and + * increment this value for each new Client. + * + *

This {@link DebugPortManager} allows applications using ddmlib to provide a custom + * port provider on a per-Client basis, depending on the device/emulator they are + * running on, and/or their names. + */ +public class DebugPortManager { + + /** + * Classes which implement this interface provide a method that provides a non random + * debugger port for a newly created {@link Client}. + */ + public interface IDebugPortProvider { + + public static final int NO_STATIC_PORT = -1; + + /** + * Returns a non-random debugger port for the specified application running on the + * specified {@link Device}. + * @param device The device the application is running on. + * @param appName The application name, as defined in the AndroidManifest.xml + * package attribute of the manifest node. + * @return The non-random debugger port or {@link #NO_STATIC_PORT} if the {@link Client} + * should use the automatic debugger port provider. + */ + public int getPort(IDevice device, String appName); + } + + private static IDebugPortProvider sProvider = null; + + /** + * Sets the {@link IDebugPortProvider} that will be used when a new {@link Client} requests + * a debugger port. + * @param provider the IDebugPortProvider to use. + */ + public static void setProvider(IDebugPortProvider provider) { + sProvider = provider; + } + + /** + * Returns the + * @return + */ + static IDebugPortProvider getProvider() { + return sProvider; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java new file mode 100644 index 000000000..cebbc32f8 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.ClientData.DebuggerStatus; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +/** + * This represents a pending or established connection with a JDWP debugger. + */ +class Debugger { + + /* + * Messages from the debugger should be pretty small; may not even + * need an expanding-buffer implementation for this. + */ + private static final int INITIAL_BUF_SIZE = 1 * 1024; + private static final int MAX_BUF_SIZE = 32 * 1024; + private ByteBuffer mReadBuffer; + + private static final int PRE_DATA_BUF_SIZE = 256; + private ByteBuffer mPreDataBuffer; + + /* connection state */ + private int mConnState; + private static final int ST_NOT_CONNECTED = 1; + private static final int ST_AWAIT_SHAKE = 2; + private static final int ST_READY = 3; + + /* peer */ + private Client mClient; // client we're forwarding to/from + private int mListenPort; // listen to me + private ServerSocketChannel mListenChannel; + + /* this goes up and down; synchronize methods that access the field */ + private SocketChannel mChannel; + + /** + * Create a new Debugger object, configured to listen for connections + * on a specific port. + */ + Debugger(Client client, int listenPort) throws IOException { + + mClient = client; + mListenPort = listenPort; + + mListenChannel = ServerSocketChannel.open(); + mListenChannel.configureBlocking(false); // required for Selector + + InetSocketAddress addr = new InetSocketAddress( + InetAddress.getByName("localhost"), // $NON-NLS-1$ + listenPort); + mListenChannel.socket().setReuseAddress(true); // enable SO_REUSEADDR + mListenChannel.socket().bind(addr); + + mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE); + mPreDataBuffer = ByteBuffer.allocate(PRE_DATA_BUF_SIZE); + mConnState = ST_NOT_CONNECTED; + + Log.d("ddms", "Created: " + this.toString()); + } + + /** + * Returns "true" if a debugger is currently attached to us. + */ + boolean isDebuggerAttached() { + return mChannel != null; + } + + /** + * Represent the Debugger as a string. + */ + @Override + public String toString() { + // mChannel != null means we have connection, ST_READY means it's going + return "[Debugger " + mListenPort + "-->" + mClient.getClientData().getPid() + + ((mConnState != ST_READY) ? " inactive]" : " active]"); + } + + /** + * Register the debugger's listen socket with the Selector. + */ + void registerListener(Selector sel) throws IOException { + mListenChannel.register(sel, SelectionKey.OP_ACCEPT, this); + } + + /** + * Return the Client being debugged. + */ + Client getClient() { + return mClient; + } + + /** + * Accept a new connection, but only if we don't already have one. + * + * Must be synchronized with other uses of mChannel and mPreBuffer. + * + * Returns "null" if we're already talking to somebody. + */ + synchronized SocketChannel accept() throws IOException { + return accept(mListenChannel); + } + + /** + * Accept a new connection from the specified listen channel. This + * is so we can listen on a dedicated port for the "current" client, + * where "current" is constantly in flux. + * + * Must be synchronized with other uses of mChannel and mPreBuffer. + * + * Returns "null" if we're already talking to somebody. + */ + synchronized SocketChannel accept(ServerSocketChannel listenChan) + throws IOException { + + if (listenChan != null) { + SocketChannel newChan; + + newChan = listenChan.accept(); + if (mChannel != null) { + Log.w("ddms", "debugger already talking to " + mClient + + " on " + mListenPort); + newChan.close(); + return null; + } + mChannel = newChan; + mChannel.configureBlocking(false); // required for Selector + mConnState = ST_AWAIT_SHAKE; + return mChannel; + } + + return null; + } + + /** + * Close the data connection only. + */ + synchronized void closeData() { + try { + if (mChannel != null) { + mChannel.close(); + mChannel = null; + mConnState = ST_NOT_CONNECTED; + + ClientData cd = mClient.getClientData(); + cd.setDebuggerConnectionStatus(DebuggerStatus.DEFAULT); + mClient.update(Client.CHANGE_DEBUGGER_STATUS); + } + } catch (IOException ioe) { + Log.w("ddms", "Failed to close data " + this); + } + } + + /** + * Close the socket that's listening for new connections and (if + * we're connected) the debugger data socket. + */ + synchronized void close() { + try { + if (mListenChannel != null) { + mListenChannel.close(); + } + mListenChannel = null; + closeData(); + } catch (IOException ioe) { + Log.w("ddms", "Failed to close listener " + this); + } + } + + // TODO: ?? add a finalizer that verifies the channel was closed + + /** + * Read data from our channel. + * + * This is called when data is known to be available, and we don't yet + * have a full packet in the buffer. If the buffer is at capacity, + * expand it. + */ + void read() throws IOException { + int count; + + if (mReadBuffer.position() == mReadBuffer.capacity()) { + if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) { + throw new BufferOverflowException(); + } + Log.d("ddms", "Expanding read buffer to " + + mReadBuffer.capacity() * 2); + + ByteBuffer newBuffer = + ByteBuffer.allocate(mReadBuffer.capacity() * 2); + mReadBuffer.position(0); + newBuffer.put(mReadBuffer); // leaves "position" at end + + mReadBuffer = newBuffer; + } + + count = mChannel.read(mReadBuffer); + Log.v("ddms", "Read " + count + " bytes from " + this); + if (count < 0) throw new IOException("read failed"); + } + + /** + * Return information for the first full JDWP packet in the buffer. + * + * If we don't yet have a full packet, return null. + * + * If we haven't yet received the JDWP handshake, we watch for it here + * and consume it without admitting to have done so. We also send + * the handshake response to the debugger, along with any pending + * pre-connection data, which is why this can throw an IOException. + */ + JdwpPacket getJdwpPacket() throws IOException { + /* + * On entry, the data starts at offset 0 and ends at "position". + * "limit" is set to the buffer capacity. + */ + if (mConnState == ST_AWAIT_SHAKE) { + int result; + + result = JdwpPacket.findHandshake(mReadBuffer); + //Log.v("ddms", "findHand: " + result); + switch (result) { + case JdwpPacket.HANDSHAKE_GOOD: + Log.d("ddms", "Good handshake from debugger"); + JdwpPacket.consumeHandshake(mReadBuffer); + sendHandshake(); + mConnState = ST_READY; + + ClientData cd = mClient.getClientData(); + cd.setDebuggerConnectionStatus(DebuggerStatus.ATTACHED); + mClient.update(Client.CHANGE_DEBUGGER_STATUS); + + // see if we have another packet in the buffer + return getJdwpPacket(); + case JdwpPacket.HANDSHAKE_BAD: + // not a debugger, throw an exception so we drop the line + Log.d("ddms", "Bad handshake from debugger"); + throw new IOException("bad handshake"); + case JdwpPacket.HANDSHAKE_NOTYET: + break; + default: + Log.e("ddms", "Unknown packet while waiting for client handshake"); + } + return null; + } else if (mConnState == ST_READY) { + if (mReadBuffer.position() != 0) { + Log.v("ddms", "Checking " + mReadBuffer.position() + " bytes"); + } + return JdwpPacket.findPacket(mReadBuffer); + } else { + Log.e("ddms", "Receiving data in state = " + mConnState); + } + + return null; + } + + /** + * Forward a packet to the client. + * + * "mClient" will never be null, though it's possible that the channel + * in the client has closed and our send attempt will fail. + * + * Consumes the packet. + */ + void forwardPacketToClient(JdwpPacket packet) throws IOException { + mClient.sendAndConsume(packet); + } + + /** + * Send the handshake to the debugger. We also send along any packets + * we already received from the client (usually just a VM_START event, + * if anything at all). + */ + private synchronized void sendHandshake() throws IOException { + ByteBuffer tempBuffer = ByteBuffer.allocate(JdwpPacket.HANDSHAKE_LEN); + JdwpPacket.putHandshake(tempBuffer); + int expectedLength = tempBuffer.position(); + tempBuffer.flip(); + if (mChannel.write(tempBuffer) != expectedLength) { + throw new IOException("partial handshake write"); + } + + expectedLength = mPreDataBuffer.position(); + if (expectedLength > 0) { + Log.d("ddms", "Sending " + mPreDataBuffer.position() + + " bytes of saved data"); + mPreDataBuffer.flip(); + if (mChannel.write(mPreDataBuffer) != expectedLength) { + throw new IOException("partial pre-data write"); + } + mPreDataBuffer.clear(); + } + } + + /** + * Send a packet to the debugger. + * + * Ideally, we can do this with a single channel write. If that doesn't + * happen, we have to prevent anybody else from writing to the channel + * until this packet completes, so we synchronize on the channel. + * + * Another goal is to avoid unnecessary buffer copies, so we write + * directly out of the JdwpPacket's ByteBuffer. + * + * We must synchronize on "mChannel" before writing to it. We want to + * coordinate the buffered data with mChannel creation, so this whole + * method is synchronized. + */ + synchronized void sendAndConsume(JdwpPacket packet) + throws IOException { + + if (mChannel == null) { + /* + * Buffer this up so we can send it to the debugger when it + * finally does connect. This is essential because the VM_START + * message might be telling the debugger that the VM is + * suspended. The alternative approach would be for us to + * capture and interpret VM_START and send it later if we + * didn't choose to un-suspend the VM for our own purposes. + */ + Log.d("ddms", "Saving packet 0x" + + Integer.toHexString(packet.getId())); + packet.movePacket(mPreDataBuffer); + } else { + packet.writeAndConsume(mChannel); + } + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java new file mode 100644 index 000000000..e23d8177a --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.SyncService.SyncResult; +import com.android.ddmlib.log.LogReceiver; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * A Device. It can be a physical device or an emulator. + */ +final class Device implements IDevice { + + /** Emulator Serial Number regexp. */ + final static String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$ + + /** Serial number of the device */ + private String mSerialNumber = null; + + /** Name of the AVD */ + private String mAvdName = null; + + /** State of the device. */ + private DeviceState mState = null; + + /** Device properties. */ + private final Map mProperties = new HashMap(); + + private final ArrayList mClients = new ArrayList(); + private DeviceMonitor mMonitor; + + private static final String LOG_TAG = "Device"; + + /** + * Socket for the connection monitoring client connection/disconnection. + */ + private SocketChannel mSocketChannel; + + /** + * Output receiver for "pm install package.apk" command line. + */ + private static final class InstallReceiver extends MultiLineReceiver { + + private static final String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$ + private static final Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$ + + private String mErrorMessage = null; + + public InstallReceiver() { + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.length() > 0) { + if (line.startsWith(SUCCESS_OUTPUT)) { + mErrorMessage = null; + } else { + Matcher m = FAILURE_PATTERN.matcher(line); + if (m.matches()) { + mErrorMessage = m.group(1); + } + } + } + } + } + + public boolean isCancelled() { + return false; + } + + public String getErrorMessage() { + return mErrorMessage; + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getSerialNumber() + */ + public String getSerialNumber() { + return mSerialNumber; + } + + /** {@inheritDoc} */ + public String getAvdName() { + return mAvdName; + } + + /** + * Sets the name of the AVD + */ + void setAvdName(String avdName) { + if (isEmulator() == false) { + throw new IllegalArgumentException( + "Cannot set the AVD name of the device is not an emulator"); + } + + mAvdName = avdName; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getState() + */ + public DeviceState getState() { + return mState; + } + + /** + * Changes the state of the device. + */ + void setState(DeviceState state) { + mState = state; + } + + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getProperties() + */ + public Map getProperties() { + return Collections.unmodifiableMap(mProperties); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getPropertyCount() + */ + public int getPropertyCount() { + return mProperties.size(); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getProperty(java.lang.String) + */ + public String getProperty(String name) { + return mProperties.get(name); + } + + + @Override + public String toString() { + return mSerialNumber; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isOnline() + */ + public boolean isOnline() { + return mState == DeviceState.ONLINE; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isEmulator() + */ + public boolean isEmulator() { + return mSerialNumber.matches(RE_EMULATOR_SN); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isOffline() + */ + public boolean isOffline() { + return mState == DeviceState.OFFLINE; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#isBootLoader() + */ + public boolean isBootLoader() { + return mState == DeviceState.BOOTLOADER; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#hasClients() + */ + public boolean hasClients() { + return mClients.size() > 0; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getClients() + */ + public Client[] getClients() { + synchronized (mClients) { + return mClients.toArray(new Client[mClients.size()]); + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getClient(java.lang.String) + */ + public Client getClient(String applicationName) { + synchronized (mClients) { + for (Client c : mClients) { + if (applicationName.equals(c.getClientData().getClientDescription())) { + return c; + } + } + + } + + return null; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getSyncService() + */ + public SyncService getSyncService() throws IOException { + SyncService syncService = new SyncService(AndroidDebugBridge.sSocketAddr, this); + if (syncService.openSync()) { + return syncService; + } + + return null; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getFileListingService() + */ + public FileListingService getFileListingService() { + return new FileListingService(this); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getScreenshot() + */ + public RawImage getScreenshot() throws IOException { + return AdbHelper.getFrameBuffer(AndroidDebugBridge.sSocketAddr, this); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#executeShellCommand(java.lang.String, com.android.ddmlib.IShellOutputReceiver) + */ + public void executeShellCommand(String command, IShellOutputReceiver receiver) + throws IOException { + AdbHelper.executeRemoteCommand(AndroidDebugBridge.sSocketAddr, command, this, + receiver); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#runEventLogService(com.android.ddmlib.log.LogReceiver) + */ + public void runEventLogService(LogReceiver receiver) throws IOException { + AdbHelper.runEventLogService(AndroidDebugBridge.sSocketAddr, this, receiver); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#runLogService(com.android.ddmlib.log.LogReceiver) + */ + public void runLogService(String logname, + LogReceiver receiver) throws IOException { + AdbHelper.runLogService(AndroidDebugBridge.sSocketAddr, this, logname, receiver); + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#createForward(int, int) + */ + public boolean createForward(int localPort, int remotePort) { + try { + return AdbHelper.createForward(AndroidDebugBridge.sSocketAddr, this, + localPort, remotePort); + } catch (IOException e) { + Log.e("adb-forward", e); //$NON-NLS-1$ + return false; + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#removeForward(int, int) + */ + public boolean removeForward(int localPort, int remotePort) { + try { + return AdbHelper.removeForward(AndroidDebugBridge.sSocketAddr, this, + localPort, remotePort); + } catch (IOException e) { + Log.e("adb-remove-forward", e); //$NON-NLS-1$ + return false; + } + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#getClientName(int) + */ + public String getClientName(int pid) { + synchronized (mClients) { + for (Client c : mClients) { + if (c.getClientData().getPid() == pid) { + return c.getClientData().getClientDescription(); + } + } + } + + return null; + } + + + Device(DeviceMonitor monitor, String serialNumber, DeviceState deviceState) { + mMonitor = monitor; + mSerialNumber = serialNumber; + mState = deviceState; + } + + DeviceMonitor getMonitor() { + return mMonitor; + } + + void addClient(Client client) { + synchronized (mClients) { + mClients.add(client); + } + } + + List getClientList() { + return mClients; + } + + boolean hasClient(int pid) { + synchronized (mClients) { + for (Client client : mClients) { + if (client.getClientData().getPid() == pid) { + return true; + } + } + } + + return false; + } + + void clearClientList() { + synchronized (mClients) { + mClients.clear(); + } + } + + /** + * Sets the client monitoring socket. + * @param socketChannel the sockets + */ + void setClientMonitoringSocket(SocketChannel socketChannel) { + mSocketChannel = socketChannel; + } + + /** + * Returns the client monitoring socket. + */ + SocketChannel getClientMonitoringSocket() { + return mSocketChannel; + } + + /** + * Removes a {@link Client} from the list. + * @param client the client to remove. + * @param notify Whether or not to notify the listeners of a change. + */ + void removeClient(Client client, boolean notify) { + mMonitor.addPortToAvailableList(client.getDebuggerListenPort()); + synchronized (mClients) { + mClients.remove(client); + } + if (notify) { + mMonitor.getServer().deviceChanged(this, CHANGE_CLIENT_LIST); + } + } + + void update(int changeMask) { + mMonitor.getServer().deviceChanged(this, changeMask); + } + + void update(Client client, int changeMask) { + mMonitor.getServer().clientChanged(client, changeMask); + } + + void addProperty(String label, String value) { + mProperties.put(label, value); + } + + /** + * {@inheritDoc} + */ + public String installPackage(String packageFilePath, boolean reinstall) + throws IOException { + String remoteFilePath = syncPackageToDevice(packageFilePath); + String result = installRemotePackage(remoteFilePath, reinstall); + removeRemotePackage(remoteFilePath); + return result; + } + + /** + * {@inheritDoc} + */ + public String syncPackageToDevice(String localFilePath) + throws IOException { + try { + String packageFileName = getFileName(localFilePath); + String remoteFilePath = String.format("/data/local/tmp/%1$s", packageFileName); //$NON-NLS-1$ + + Log.d(packageFileName, String.format("Uploading %1$s onto device '%2$s'", + packageFileName, getSerialNumber())); + + SyncService sync = getSyncService(); + if (sync != null) { + String message = String.format("Uploading file onto device '%1$s'", + getSerialNumber()); + Log.d(LOG_TAG, message); + SyncResult result = sync.pushFile(localFilePath, remoteFilePath, + SyncService.getNullProgressMonitor()); + + if (result.getCode() != SyncService.RESULT_OK) { + throw new IOException(String.format("Unable to upload file: %1$s", + result.getMessage())); + } + } else { + throw new IOException("Unable to open sync connection!"); + } + return remoteFilePath; + } catch (IOException e) { + Log.e(LOG_TAG, String.format("Unable to open sync connection! reason: %1$s", + e.getMessage())); + throw e; + } + } + + /** + * Helper method to retrieve the file name given a local file path + * @param filePath full directory path to file + * @return {@link String} file name + */ + private String getFileName(String filePath) { + return new File(filePath).getName(); + } + + /** + * {@inheritDoc} + */ + public String installRemotePackage(String remoteFilePath, boolean reinstall) + throws IOException { + InstallReceiver receiver = new InstallReceiver(); + String cmd = String.format(reinstall ? "pm install -r \"%1$s\"" : "pm install \"%1$s\"", + remoteFilePath); + executeShellCommand(cmd, receiver); + return receiver.getErrorMessage(); + } + + /** + * {@inheritDoc} + */ + public void removeRemotePackage(String remoteFilePath) throws IOException { + // now we delete the app we sync'ed + try { + executeShellCommand("rm " + remoteFilePath, new NullOutputReceiver()); + } catch (IOException e) { + Log.e(LOG_TAG, String.format("Failed to delete temporary package: %1$s", + e.getMessage())); + throw e; + } + } + + /** + * {@inheritDoc} + */ + public String uninstallPackage(String packageName) throws IOException { + InstallReceiver receiver = new InstallReceiver(); + executeShellCommand("pm uninstall " + packageName, receiver); + return receiver.getErrorMessage(); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java new file mode 100644 index 000000000..402699cf5 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java @@ -0,0 +1,867 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.AdbHelper.AdbResponse; +import com.android.ddmlib.ClientData.DebuggerStatus; +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.IDevice.DeviceState; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * A Device monitor. This connects to the Android Debug Bridge and get device and + * debuggable process information from it. + */ +final class DeviceMonitor { + private byte[] mLengthBuffer = new byte[4]; + private byte[] mLengthBuffer2 = new byte[4]; + + private boolean mQuit = false; + + private AndroidDebugBridge mServer; + + private SocketChannel mMainAdbConnection = null; + private boolean mMonitoring = false; + private int mConnectionAttempt = 0; + private int mRestartAttemptCount = 0; + private boolean mInitialDeviceListDone = false; + + private Selector mSelector; + + private final ArrayList mDevices = new ArrayList(); + + private final ArrayList mDebuggerPorts = new ArrayList(); + + private final HashMap mClientsToReopen = new HashMap(); + + /** + * Creates a new {@link DeviceMonitor} object and links it to the running + * {@link AndroidDebugBridge} object. + * @param server the running {@link AndroidDebugBridge}. + */ + DeviceMonitor(AndroidDebugBridge server) { + mServer = server; + + mDebuggerPorts.add(DdmPreferences.getDebugPortBase()); + } + + /** + * Starts the monitoring. + */ + void start() { + new Thread("Device List Monitor") { //$NON-NLS-1$ + @Override + public void run() { + deviceMonitorLoop(); + } + }.start(); + } + + /** + * Stops the monitoring. + */ + void stop() { + mQuit = true; + + // wakeup the main loop thread by closing the main connection to adb. + try { + if (mMainAdbConnection != null) { + mMainAdbConnection.close(); + } + } catch (IOException e1) { + } + + // wake up the secondary loop by closing the selector. + if (mSelector != null) { + mSelector.wakeup(); + } + } + + + + /** + * Returns if the monitor is currently connected to the debug bridge server. + * @return + */ + boolean isMonitoring() { + return mMonitoring; + } + + int getConnectionAttemptCount() { + return mConnectionAttempt; + } + + int getRestartAttemptCount() { + return mRestartAttemptCount; + } + + /** + * Returns the devices. + */ + Device[] getDevices() { + synchronized (mDevices) { + return mDevices.toArray(new Device[mDevices.size()]); + } + } + + boolean hasInitialDeviceList() { + return mInitialDeviceListDone; + } + + AndroidDebugBridge getServer() { + return mServer; + } + + void addClientToDropAndReopen(Client client, int port) { + synchronized (mClientsToReopen) { + Log.d("DeviceMonitor", + "Adding " + client + " to list of client to reopen (" + port +")."); + if (mClientsToReopen.get(client) == null) { + mClientsToReopen.put(client, port); + } + } + mSelector.wakeup(); + } + + /** + * Monitors the devices. This connects to the Debug Bridge + */ + private void deviceMonitorLoop() { + do { + try { + if (mMainAdbConnection == null) { + Log.d("DeviceMonitor", "Opening adb connection"); + mMainAdbConnection = openAdbConnection(); + if (mMainAdbConnection == null) { + mConnectionAttempt++; + Log.e("DeviceMonitor", "Connection attempts: " + mConnectionAttempt); + if (mConnectionAttempt > 10) { + if (mServer.startAdb() == false) { + mRestartAttemptCount++; + Log.e("DeviceMonitor", + "adb restart attempts: " + mRestartAttemptCount); + } else { + mRestartAttemptCount = 0; + } + } + waitABit(); + } else { + Log.d("DeviceMonitor", "Connected to adb for device monitoring"); + mConnectionAttempt = 0; + } + } + + if (mMainAdbConnection != null && mMonitoring == false) { + mMonitoring = sendDeviceListMonitoringRequest(); + } + + if (mMonitoring) { + // read the length of the incoming message + int length = readLength(mMainAdbConnection, mLengthBuffer); + + if (length >= 0) { + // read the incoming message + processIncomingDeviceData(length); + + // flag the fact that we have build the list at least once. + mInitialDeviceListDone = true; + } + } + } catch (AsynchronousCloseException ace) { + // this happens because of a call to Quit. We do nothing, and the loop will break. + } catch (IOException ioe) { + if (mQuit == false) { + Log.e("DeviceMonitor", "Adb connection Error:" + ioe.getMessage()); + mMonitoring = false; + if (mMainAdbConnection != null) { + try { + mMainAdbConnection.close(); + } catch (IOException ioe2) { + // we can safely ignore that one. + } + mMainAdbConnection = null; + } + } + } + } while (mQuit == false); + } + + /** + * Sleeps for a little bit. + */ + private void waitABit() { + try { + Thread.sleep(1000); + } catch (InterruptedException e1) { + } + } + + /** + * Attempts to connect to the debug bridge server. + * @return a connect socket if success, null otherwise + */ + private SocketChannel openAdbConnection() { + Log.d("DeviceMonitor", "Connecting to adb for Device List Monitoring..."); + + SocketChannel adbChannel = null; + try { + adbChannel = SocketChannel.open(AndroidDebugBridge.sSocketAddr); + adbChannel.socket().setTcpNoDelay(true); + } catch (IOException e) { + } + + return adbChannel; + } + + /** + * + * @return + * @throws IOException + */ + private boolean sendDeviceListMonitoringRequest() throws IOException { + byte[] request = AdbHelper.formAdbRequest("host:track-devices"); //$NON-NLS-1$ + + if (AdbHelper.write(mMainAdbConnection, request) == false) { + Log.e("DeviceMonitor", "Sending Tracking request failed!"); + mMainAdbConnection.close(); + throw new IOException("Sending Tracking request failed!"); + } + + AdbResponse resp = AdbHelper.readAdbResponse(mMainAdbConnection, + false /* readDiagString */); + + if (resp.ioSuccess == false) { + Log.e("DeviceMonitor", "Failed to read the adb response!"); + mMainAdbConnection.close(); + throw new IOException("Failed to read the adb response!"); + } + + if (resp.okay == false) { + // request was refused by adb! + Log.e("DeviceMonitor", "adb refused request: " + resp.message); + } + + return resp.okay; + } + + /** + * Processes an incoming device message from the socket + * @param socket + * @param length + * @throws IOException + */ + private void processIncomingDeviceData(int length) throws IOException { + ArrayList list = new ArrayList(); + + if (length > 0) { + byte[] buffer = new byte[length]; + String result = read(mMainAdbConnection, buffer); + + String[] devices = result.split("\n"); // $NON-NLS-1$ + + for (String d : devices) { + String[] param = d.split("\t"); // $NON-NLS-1$ + if (param.length == 2) { + // new adb uses only serial numbers to identify devices + Device device = new Device(this, param[0] /*serialnumber*/, + DeviceState.getState(param[1])); + + //add the device to the list + list.add(device); + } + } + } + + // now merge the new devices with the old ones. + updateDevices(list); + } + + /** + * Updates the device list with the new items received from the monitoring service. + */ + private void updateDevices(ArrayList newList) { + // because we are going to call mServer.deviceDisconnected which will acquire this lock + // we lock it first, so that the AndroidDebugBridge lock is always locked first. + synchronized (AndroidDebugBridge.getLock()) { + synchronized (mDevices) { + // For each device in the current list, we look for a matching the new list. + // * if we find it, we update the current object with whatever new information + // there is + // (mostly state change, if the device becomes ready, we query for build info). + // We also remove the device from the new list to mark it as "processed" + // * if we do not find it, we remove it from the current list. + // Once this is done, the new list contains device we aren't monitoring yet, so we + // add them to the list, and start monitoring them. + + for (int d = 0 ; d < mDevices.size() ;) { + Device device = mDevices.get(d); + + // look for a similar device in the new list. + int count = newList.size(); + boolean foundMatch = false; + for (int dd = 0 ; dd < count ; dd++) { + Device newDevice = newList.get(dd); + // see if it matches in id and serial number. + if (newDevice.getSerialNumber().equals(device.getSerialNumber())) { + foundMatch = true; + + // update the state if needed. + if (device.getState() != newDevice.getState()) { + device.setState(newDevice.getState()); + device.update(Device.CHANGE_STATE); + + // if the device just got ready/online, we need to start + // monitoring it. + if (device.isOnline()) { + if (AndroidDebugBridge.getClientSupport() == true) { + if (startMonitoringDevice(device) == false) { + Log.e("DeviceMonitor", + "Failed to start monitoring " + + device.getSerialNumber()); + } + } + + if (device.getPropertyCount() == 0) { + queryNewDeviceForInfo(device); + } + } + } + + // remove the new device from the list since it's been used + newList.remove(dd); + break; + } + } + + if (foundMatch == false) { + // the device is gone, we need to remove it, and keep current index + // to process the next one. + removeDevice(device); + mServer.deviceDisconnected(device); + } else { + // process the next one + d++; + } + } + + // at this point we should still have some new devices in newList, so we + // process them. + for (Device newDevice : newList) { + // add them to the list + mDevices.add(newDevice); + mServer.deviceConnected(newDevice); + + // start monitoring them. + if (AndroidDebugBridge.getClientSupport() == true) { + if (newDevice.isOnline()) { + startMonitoringDevice(newDevice); + } + } + + // look for their build info. + if (newDevice.isOnline()) { + queryNewDeviceForInfo(newDevice); + } + } + } + } + newList.clear(); + } + + private void removeDevice(Device device) { + device.clearClientList(); + mDevices.remove(device); + + SocketChannel channel = device.getClientMonitoringSocket(); + if (channel != null) { + try { + channel.close(); + } catch (IOException e) { + // doesn't really matter if the close fails. + } + } + } + + /** + * Queries a device for its build info. + * @param device the device to query. + */ + private void queryNewDeviceForInfo(Device device) { + // TODO: do this in a separate thread. + try { + // first get the list of properties. + device.executeShellCommand(GetPropReceiver.GETPROP_COMMAND, + new GetPropReceiver(device)); + + // now get the emulator Virtual Device name (if applicable). + if (device.isEmulator()) { + EmulatorConsole console = EmulatorConsole.getConsole(device); + if (console != null) { + device.setAvdName(console.getAvdName()); + } + } + } catch (IOException e) { + // if we can't get the build info, it doesn't matter too much + } + } + + /** + * Starts a monitoring service for a device. + * @param device the device to monitor. + * @return true if success. + */ + private boolean startMonitoringDevice(Device device) { + SocketChannel socketChannel = openAdbConnection(); + + if (socketChannel != null) { + try { + boolean result = sendDeviceMonitoringRequest(socketChannel, device); + if (result) { + + if (mSelector == null) { + startDeviceMonitorThread(); + } + + device.setClientMonitoringSocket(socketChannel); + + synchronized (mDevices) { + // always wakeup before doing the register. The synchronized block + // ensure that the selector won't select() before the end of this block. + // @see deviceClientMonitorLoop + mSelector.wakeup(); + + socketChannel.configureBlocking(false); + socketChannel.register(mSelector, SelectionKey.OP_READ, device); + } + + return true; + } + } catch (IOException e) { + try { + // attempt to close the socket if needed. + socketChannel.close(); + } catch (IOException e1) { + // we can ignore that one. It may already have been closed. + } + Log.d("DeviceMonitor", + "Connection Failure when starting to monitor device '" + + device + "' : " + e.getMessage()); + } + } + + return false; + } + + private void startDeviceMonitorThread() throws IOException { + mSelector = Selector.open(); + new Thread("Device Client Monitor") { //$NON-NLS-1$ + @Override + public void run() { + deviceClientMonitorLoop(); + } + }.start(); + } + + private void deviceClientMonitorLoop() { + do { + try { + // This synchronized block stops us from doing the select() if a new + // Device is being added. + // @see startMonitoringDevice() + synchronized (mDevices) { + } + + int count = mSelector.select(); + + if (mQuit) { + return; + } + + synchronized (mClientsToReopen) { + if (mClientsToReopen.size() > 0) { + Set clients = mClientsToReopen.keySet(); + MonitorThread monitorThread = MonitorThread.getInstance(); + + for (Client client : clients) { + Device device = client.getDeviceImpl(); + int pid = client.getClientData().getPid(); + + monitorThread.dropClient(client, false /* notify */); + + // This is kinda bad, but if we don't wait a bit, the client + // will never answer the second handshake! + waitABit(); + + int port = mClientsToReopen.get(client); + + if (port == IDebugPortProvider.NO_STATIC_PORT) { + port = getNextDebuggerPort(); + } + Log.d("DeviceMonitor", "Reopening " + client); + openClient(device, pid, port, monitorThread); + device.update(Device.CHANGE_CLIENT_LIST); + } + + mClientsToReopen.clear(); + } + } + + if (count == 0) { + continue; + } + + Set keys = mSelector.selectedKeys(); + Iterator iter = keys.iterator(); + + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + + if (key.isValid() && key.isReadable()) { + Object attachment = key.attachment(); + + if (attachment instanceof Device) { + Device device = (Device)attachment; + + SocketChannel socket = device.getClientMonitoringSocket(); + + if (socket != null) { + try { + int length = readLength(socket, mLengthBuffer2); + + processIncomingJdwpData(device, socket, length); + } catch (IOException ioe) { + Log.d("DeviceMonitor", + "Error reading jdwp list: " + ioe.getMessage()); + socket.close(); + + // restart the monitoring of that device + synchronized (mDevices) { + if (mDevices.contains(device)) { + Log.d("DeviceMonitor", + "Restarting monitoring service for " + device); + startMonitoringDevice(device); + } + } + } + } + } + } + } + } catch (IOException e) { + if (mQuit == false) { + + } + } + + } while (mQuit == false); + } + + private boolean sendDeviceMonitoringRequest(SocketChannel socket, Device device) + throws IOException { + + AdbHelper.setDevice(socket, device); + + byte[] request = AdbHelper.formAdbRequest("track-jdwp"); //$NON-NLS-1$ + + if (AdbHelper.write(socket, request) == false) { + Log.e("DeviceMonitor", "Sending jdwp tracking request failed!"); + socket.close(); + throw new IOException(); + } + + AdbResponse resp = AdbHelper.readAdbResponse(socket, false /* readDiagString */); + + if (resp.ioSuccess == false) { + Log.e("DeviceMonitor", "Failed to read the adb response!"); + socket.close(); + throw new IOException(); + } + + if (resp.okay == false) { + // request was refused by adb! + Log.e("DeviceMonitor", "adb refused request: " + resp.message); + } + + return resp.okay; + } + + private void processIncomingJdwpData(Device device, SocketChannel monitorSocket, int length) + throws IOException { + if (length >= 0) { + // array for the current pids. + ArrayList pidList = new ArrayList(); + + // get the string data if there are any + if (length > 0) { + byte[] buffer = new byte[length]; + String result = read(monitorSocket, buffer); + + // split each line in its own list and create an array of integer pid + String[] pids = result.split("\n"); //$NON-NLS-1$ + + for (String pid : pids) { + try { + pidList.add(Integer.valueOf(pid)); + } catch (NumberFormatException nfe) { + // looks like this pid is not really a number. Lets ignore it. + continue; + } + } + } + + MonitorThread monitorThread = MonitorThread.getInstance(); + + // Now we merge the current list with the old one. + // this is the same mechanism as the merging of the device list. + + // For each client in the current list, we look for a matching the pid in the new list. + // * if we find it, we do nothing, except removing the pid from its list, + // to mark it as "processed" + // * if we do not find any match, we remove the client from the current list. + // Once this is done, the new list contains pids for which we don't have clients yet, + // so we create clients for them, add them to the list, and start monitoring them. + + List clients = device.getClientList(); + + boolean changed = false; + + // because MonitorThread#dropClient acquires first the monitorThread lock and then the + // Device client list lock (when removing the Client from the list), we have to make + // sure we acquire the locks in the same order, since another thread (MonitorThread), + // could call dropClient itself. + synchronized (monitorThread) { + synchronized (clients) { + for (int c = 0 ; c < clients.size() ;) { + Client client = clients.get(c); + int pid = client.getClientData().getPid(); + + // look for a matching pid + Integer match = null; + for (Integer matchingPid : pidList) { + if (pid == matchingPid.intValue()) { + match = matchingPid; + break; + } + } + + if (match != null) { + pidList.remove(match); + c++; // move on to the next client. + } else { + // we need to drop the client. the client will remove itself from the + // list of its device which is 'clients', so there's no need to + // increment c. + // We ask the monitor thread to not send notification, as we'll do + // it once at the end. + monitorThread.dropClient(client, false /* notify */); + changed = true; + } + } + } + } + + // at this point whatever pid is left in the list needs to be converted into Clients. + for (int newPid : pidList) { + openClient(device, newPid, getNextDebuggerPort(), monitorThread); + changed = true; + } + + if (changed) { + mServer.deviceChanged(device, Device.CHANGE_CLIENT_LIST); + } + } + } + + /** + * Opens and creates a new client. + * @return + */ + private void openClient(Device device, int pid, int port, MonitorThread monitorThread) { + + SocketChannel clientSocket; + try { + clientSocket = AdbHelper.createPassThroughConnection( + AndroidDebugBridge.sSocketAddr, device, pid); + + // required for Selector + clientSocket.configureBlocking(false); + } catch (UnknownHostException uhe) { + Log.d("DeviceMonitor", "Unknown Jdwp pid: " + pid); + return; + } catch (IOException ioe) { + Log.w("DeviceMonitor", + "Failed to connect to client '" + pid + "': " + ioe.getMessage()); + return ; + } + + createClient(device, pid, clientSocket, port, monitorThread); + } + + /** + * Creates a client and register it to the monitor thread + * @param device + * @param pid + * @param socket + * @param debuggerPort the debugger port. + * @param monitorThread the {@link MonitorThread} object. + */ + private void createClient(Device device, int pid, SocketChannel socket, int debuggerPort, + MonitorThread monitorThread) { + + /* + * Successfully connected to something. Create a Client object, add + * it to the list, and initiate the JDWP handshake. + */ + + Client client = new Client(device, socket, pid); + + if (client.sendHandshake()) { + try { + if (AndroidDebugBridge.getClientSupport()) { + client.listenForDebugger(debuggerPort); + } + } catch (IOException ioe) { + client.getClientData().setDebuggerConnectionStatus(DebuggerStatus.ERROR); + Log.e("ddms", "Can't bind to local " + debuggerPort + " for debugger"); + // oh well + } + + client.requestAllocationStatus(); + } else { + Log.e("ddms", "Handshake with " + client + " failed!"); + /* + * The handshake send failed. We could remove it now, but if the + * failure is "permanent" we'll just keep banging on it and + * getting the same result. Keep it in the list with its "error" + * state so we don't try to reopen it. + */ + } + + if (client.isValid()) { + device.addClient(client); + monitorThread.addClient(client); + } else { + client = null; + } + } + + private int getNextDebuggerPort() { + // get the first port and remove it + synchronized (mDebuggerPorts) { + if (mDebuggerPorts.size() > 0) { + int port = mDebuggerPorts.get(0); + + // remove it. + mDebuggerPorts.remove(0); + + // if there's nothing left, add the next port to the list + if (mDebuggerPorts.size() == 0) { + mDebuggerPorts.add(port+1); + } + + return port; + } + } + + return -1; + } + + void addPortToAvailableList(int port) { + if (port > 0) { + synchronized (mDebuggerPorts) { + // because there could be case where clients are closed twice, we have to make + // sure the port number is not already in the list. + if (mDebuggerPorts.indexOf(port) == -1) { + // add the port to the list while keeping it sorted. It's not like there's + // going to be tons of objects so we do it linearly. + int count = mDebuggerPorts.size(); + for (int i = 0 ; i < count ; i++) { + if (port < mDebuggerPorts.get(i)) { + mDebuggerPorts.add(i, port); + break; + } + } + // TODO: check if we can compact the end of the list. + } + } + } + } + + /** + * Reads the length of the next message from a socket. + * @param socket The {@link SocketChannel} to read from. + * @return the length, or 0 (zero) if no data is available from the socket. + * @throws IOException if the connection failed. + */ + private int readLength(SocketChannel socket, byte[] buffer) throws IOException { + String msg = read(socket, buffer); + + if (msg != null) { + try { + return Integer.parseInt(msg, 16); + } catch (NumberFormatException nfe) { + // we'll throw an exception below. + } + } + + // we receive something we can't read. It's better to reset the connection at this point. + throw new IOException("Unable to read length"); + } + + /** + * Fills a buffer from a socket. + * @param socket + * @param buffer + * @return the content of the buffer as a string, or null if it failed to convert the buffer. + * @throws IOException + */ + private String read(SocketChannel socket, byte[] buffer) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(buffer, 0, buffer.length); + + while (buf.position() != buf.limit()) { + int count; + + count = socket.read(buf); + if (count < 0) { + throw new IOException("EOF"); + } + } + + try { + return new String(buffer, 0, buf.position(), AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + // we'll return null below. + } + + return null; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java new file mode 100644 index 000000000..6ac019de4 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java @@ -0,0 +1,751 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.InvalidParameterException; +import java.util.Calendar; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides control over emulated hardware of the Android emulator. + *

This is basically a wrapper around the command line console normally used with telnet. + *

+ * Regarding line termination handling:
+ * One of the issues is that the telnet protocol requires usage of \r\n. Most + * implementations don't enforce it (the dos one does). In this particular case, this is mostly + * irrelevant since we don't use telnet in Java, but that means we want to make + * sure we use the same line termination than what the console expects. The console + * code removes \r and waits for \n. + *

However this means you may receive \r\n when reading from the console. + *

+ * This API will change in the near future. + */ +public final class EmulatorConsole { + + private final static String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$ + + private final static int WAIT_TIME = 5; // spin-wait sleep, in ms + + private final static int STD_TIMEOUT = 5000; // standard delay, in ms + + private final static String HOST = "127.0.0.1"; //$NON-NLS-1$ + + private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$ + private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$ + private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$ + private final static String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$ + private final static String COMMAND_GPS = + "geo nmea $GPGGA,%1$02d%2$02d%3$02d.%4$03d," + //$NON-NLS-1$ + "%5$03d%6$09.6f,%7$c,%8$03d%9$09.6f,%10$c," + //$NON-NLS-1$ + "1,10,0.0,0.0,0,0.0,0,0.0,0000\r\n"; //$NON-NLS-1$ + + private final static Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$ + + /** + * Array of delay values: no delay, gprs, edge/egprs, umts/3d + */ + public final static int[] MIN_LATENCIES = new int[] { + 0, // No delay + 150, // gprs + 80, // edge/egprs + 35 // umts/3g + }; + + /** + * Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa. + */ + public final int[] DOWNLOAD_SPEEDS = new int[] { + 0, // full speed + 14400, // gsm + 43200, // hscsd + 80000, // gprs + 236800, // edge/egprs + 1920000, // umts/3g + 14400000 // hsdpa + }; + + /** Arrays of valid network speeds */ + public final static String[] NETWORK_SPEEDS = new String[] { + "full", //$NON-NLS-1$ + "gsm", //$NON-NLS-1$ + "hscsd", //$NON-NLS-1$ + "gprs", //$NON-NLS-1$ + "edge", //$NON-NLS-1$ + "umts", //$NON-NLS-1$ + "hsdpa", //$NON-NLS-1$ + }; + + /** Arrays of valid network latencies */ + public final static String[] NETWORK_LATENCIES = new String[] { + "none", //$NON-NLS-1$ + "gprs", //$NON-NLS-1$ + "edge", //$NON-NLS-1$ + "umts", //$NON-NLS-1$ + }; + + /** Gsm Mode enum. */ + public static enum GsmMode { + UNKNOWN((String)null), + UNREGISTERED(new String[] { "unregistered", "off" }), + HOME(new String[] { "home", "on" }), + ROAMING("roaming"), + SEARCHING("searching"), + DENIED("denied"); + + private final String[] tags; + + GsmMode(String tag) { + if (tag != null) { + this.tags = new String[] { tag }; + } else { + this.tags = new String[0]; + } + } + + GsmMode(String[] tags) { + this.tags = tags; + } + + public static GsmMode getEnum(String tag) { + for (GsmMode mode : values()) { + for (String t : mode.tags) { + if (t.equals(tag)) { + return mode; + } + } + } + return UNKNOWN; + } + + /** + * Returns the first tag of the enum. + */ + public String getTag() { + if (tags.length > 0) { + return tags[0]; + } + return null; + } + } + + public final static String RESULT_OK = null; + + private final static Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN); + private final static Pattern sVoiceStatusRegexp = Pattern.compile( + "gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private final static Pattern sDataStatusRegexp = Pattern.compile( + "gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private final static Pattern sDownloadSpeedRegexp = Pattern.compile( + "\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + private final static Pattern sMinLatencyRegexp = Pattern.compile( + "\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + private final static HashMap sEmulators = + new HashMap(); + + /** Gsm Status class */ + public static class GsmStatus { + /** Voice status. */ + public GsmMode voice = GsmMode.UNKNOWN; + /** Data status. */ + public GsmMode data = GsmMode.UNKNOWN; + } + + /** Network Status class */ + public static class NetworkStatus { + /** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */ + public int speed = -1; + /** network latency status. This is an index in the {@link #MIN_LATENCIES} array. */ + public int latency = -1; + } + + private int mPort; + + private SocketChannel mSocketChannel; + + private byte[] mBuffer = new byte[1024]; + + /** + * Returns an {@link EmulatorConsole} object for the given {@link Device}. This can + * be an already existing console, or a new one if it hadn't been created yet. + * @param d The device that the console links to. + * @return an EmulatorConsole object or null if the connection failed. + */ + public static synchronized EmulatorConsole getConsole(IDevice d) { + // we need to make sure that the device is an emulator + Matcher m = sEmulatorRegexp.matcher(d.getSerialNumber()); + if (m.matches()) { + // get the port number. This is the console port. + int port; + try { + port = Integer.parseInt(m.group(1)); + if (port <= 0) { + return null; + } + } catch (NumberFormatException e) { + // looks like we failed to get the port number. This is a bit strange since + // it's coming from a regexp that only accept digit, but we handle the case + // and return null. + return null; + } + + EmulatorConsole console = sEmulators.get(port); + + if (console != null) { + // if the console exist, we ping the emulator to check the connection. + if (console.ping() == false) { + RemoveConsole(console.mPort); + console = null; + } + } + + if (console == null) { + // no console object exists for this port so we create one, and start + // the connection. + console = new EmulatorConsole(port); + if (console.start()) { + sEmulators.put(port, console); + } else { + console = null; + } + } + + return console; + } + + return null; + } + + /** + * Removes the console object associated with a port from the map. + * @param port The port of the console to remove. + */ + private static synchronized void RemoveConsole(int port) { + sEmulators.remove(port); + } + + private EmulatorConsole(int port) { + super(); + mPort = port; + } + + /** + * Starts the connection of the console. + * @return true if success. + */ + private boolean start() { + + InetSocketAddress socketAddr; + try { + InetAddress hostAddr = InetAddress.getByName(HOST); + socketAddr = new InetSocketAddress(hostAddr, mPort); + } catch (UnknownHostException e) { + return false; + } + + try { + mSocketChannel = SocketChannel.open(socketAddr); + } catch (IOException e1) { + return false; + } + + // read some stuff from it + readLines(); + + return true; + } + + /** + * Ping the emulator to check if the connection is still alive. + * @return true if the connection is alive. + */ + private synchronized boolean ping() { + // it looks like we can send stuff, even when the emulator quit, but we can't read + // from the socket. So we check the return of readLines() + if (sendCommand(COMMAND_PING)) { + return readLines() != null; + } + + return false; + } + + /** + * Sends a KILL command to the emulator. + */ + public synchronized void kill() { + if (sendCommand(COMMAND_KILL)) { + RemoveConsole(mPort); + } + } + + public synchronized String getAvdName() { + if (sendCommand(COMMAND_AVD_NAME)) { + String[] result = readLines(); + if (result != null && result.length == 2) { // this should be the name on first line, + // and ok on 2nd line + return result[0]; + } else { + // try to see if there's a message after KO + Matcher m = RE_KO.matcher(result[result.length-1]); + if (m.matches()) { + return m.group(1); + } + } + } + + return null; + } + + /** + * Get the network status of the emulator. + * @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or + * null if the query failed. + */ + public synchronized NetworkStatus getNetworkStatus() { + if (sendCommand(COMMAND_NETWORK_STATUS)) { + /* Result is in the format + Current network status: + download speed: 14400 bits/s (1.8 KB/s) + upload speed: 14400 bits/s (1.8 KB/s) + minimum latency: 0 ms + maximum latency: 0 ms + */ + String[] result = readLines(); + + if (isValid(result)) { + // we only compare agains the min latency and the download speed + // let's not rely on the order of the output, and simply loop through + // the line testing the regexp. + NetworkStatus status = new NetworkStatus(); + for (String line : result) { + Matcher m = sDownloadSpeedRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.speed = getSpeedIndex(value); + + // move on to next line. + continue; + } + + m = sMinLatencyRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.latency = getLatencyIndex(value); + + // move on to next line. + continue; + } + } + + return status; + } + } + + return null; + } + + /** + * Returns the current gsm status of the emulator + * @return a {@link GsmStatus} object containing the gms status, or null + * if the query failed. + */ + public synchronized GsmStatus getGsmStatus() { + if (sendCommand(COMMAND_GSM_STATUS)) { + /* + * result is in the format: + * gsm status + * gsm voice state: home + * gsm data state: home + */ + + String[] result = readLines(); + if (isValid(result)) { + + GsmStatus status = new GsmStatus(); + + // let's not rely on the order of the output, and simply loop through + // the line testing the regexp. + for (String line : result) { + Matcher m = sVoiceStatusRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.voice = GsmMode.getEnum(value.toLowerCase()); + + // move on to next line. + continue; + } + + m = sDataStatusRegexp.matcher(line); + if (m.matches()) { + // get the string value + String value = m.group(1); + + // get the index from the list + status.data = GsmMode.getEnum(value.toLowerCase()); + + // move on to next line. + continue; + } + } + + return status; + } + } + + return null; + } + + /** + * Sets the GSM voice mode. + * @param mode the {@link GsmMode} value. + * @return RESULT_OK if success, an error String otherwise. + * @throws InvalidParameterException if mode is an invalid value. + */ + public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException { + if (mode == GsmMode.UNKNOWN) { + throw new InvalidParameterException(); + } + + String command = String.format(COMMAND_GSM_VOICE, mode.getTag()); + return processCommand(command); + } + + /** + * Sets the GSM data mode. + * @param mode the {@link GsmMode} value + * @return {@link #RESULT_OK} if success, an error String otherwise. + * @throws InvalidParameterException if mode is an invalid value. + */ + public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException { + if (mode == GsmMode.UNKNOWN) { + throw new InvalidParameterException(); + } + + String command = String.format(COMMAND_GSM_DATA, mode.getTag()); + return processCommand(command); + } + + /** + * Initiate an incoming call on the emulator. + * @param number a string representing the calling number. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String call(String number) { + String command = String.format(COMMAND_GSM_CALL, number); + return processCommand(command); + } + + /** + * Cancels a current call. + * @param number the number of the call to cancel + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String cancelCall(String number) { + String command = String.format(COMMAND_GSM_CANCEL_CALL, number); + return processCommand(command); + } + + /** + * Sends an SMS to the emulator + * @param number The sender phone number + * @param message The SMS message. \ characters must be escaped. The carriage return is + * the 2 character sequence {'\', 'n' } + * + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String sendSms(String number, String message) { + String command = String.format(COMMAND_SMS_SEND, number, message); + return processCommand(command); + } + + /** + * Sets the network speed. + * @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String setNetworkSpeed(int selectionIndex) { + String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]); + return processCommand(command); + } + + /** + * Sets the network latency. + * @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table. + * @return {@link #RESULT_OK} if success, an error String otherwise. + */ + public synchronized String setNetworkLatency(int selectionIndex) { + String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]); + return processCommand(command); + } + + public synchronized String sendLocation(double longitude, double latitude, double elevation) { + + Calendar c = Calendar.getInstance(); + + double absLong = Math.abs(longitude); + int longDegree = (int)Math.floor(absLong); + char longDirection = 'E'; + if (longitude < 0) { + longDirection = 'W'; + } + + double longMinute = (absLong - Math.floor(absLong)) * 60; + + double absLat = Math.abs(latitude); + int latDegree = (int)Math.floor(absLat); + char latDirection = 'N'; + if (latitude < 0) { + latDirection = 'S'; + } + + double latMinute = (absLat - Math.floor(absLat)) * 60; + + String command = String.format(COMMAND_GPS, + c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), + c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND), + latDegree, latMinute, latDirection, + longDegree, longMinute, longDirection); + + return processCommand(command); + } + + /** + * Sends a command to the emulator console. + * @param command The command string. MUST BE TERMINATED BY \n. + * @return true if success + */ + private boolean sendCommand(String command) { + boolean result = false; + try { + byte[] bCommand; + try { + bCommand = command.getBytes(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + // wrong encoding... + return result; + } + + // write the command + AdbHelper.write(mSocketChannel, bCommand, bCommand.length, DdmPreferences.getTimeOut()); + + result = true; + } catch (IOException e) { + return false; + } finally { + if (result == false) { + // FIXME connection failed somehow, we need to disconnect the console. + RemoveConsole(mPort); + } + } + + return result; + } + + /** + * Sends a command to the emulator and parses its answer. + * @param command the command to send. + * @return {@link #RESULT_OK} if success, an error message otherwise. + */ + private String processCommand(String command) { + if (sendCommand(command)) { + String[] result = readLines(); + + if (result != null && result.length > 0) { + Matcher m = RE_KO.matcher(result[result.length-1]); + if (m.matches()) { + return m.group(1); + } + return RESULT_OK; + } + + return "Unable to communicate with the emulator"; + } + + return "Unable to send command to the emulator"; + } + + /** + * Reads line from the console socket. This call is blocking until we read the lines: + *

    + *
  • OK\r\n
  • + *
  • KO\r\n
  • + *
+ * @return the array of strings read from the emulator. + */ + private String[] readLines() { + try { + ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length); + int numWaits = 0; + boolean stop = false; + + while (buf.position() != buf.limit() && stop == false) { + int count; + + count = mSocketChannel.read(buf); + if (count < 0) { + return null; + } else if (count == 0) { + if (numWaits * WAIT_TIME > STD_TIMEOUT) { + return null; + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + + // check the last few char aren't OK. For a valid message to test + // we need at least 4 bytes (OK/KO + \r\n) + if (buf.position() >= 4) { + int pos = buf.position(); + if (endsWithOK(pos) || lastLineIsKO(pos)) { + stop = true; + } + } + } + + String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING); + return msg.split("\r\n"); //$NON-NLS-1$ + } catch (IOException e) { + return null; + } + } + + /** + * Returns true if the 4 characters *before* the current position are "OK\r\n" + * @param currentPosition The current position + */ + private boolean endsWithOK(int currentPosition) { + if (mBuffer[currentPosition-1] == '\n' && + mBuffer[currentPosition-2] == '\r' && + mBuffer[currentPosition-3] == 'K' && + mBuffer[currentPosition-4] == 'O') { + return true; + } + + return false; + } + + /** + * Returns true if the last line starts with KO and is also terminated by \r\n + * @param currentPosition the current position + */ + private boolean lastLineIsKO(int currentPosition) { + // first check that the last 2 characters are CRLF + if (mBuffer[currentPosition-1] != '\n' || + mBuffer[currentPosition-2] != '\r') { + return false; + } + + // now loop backward looking for the previous CRLF, or the beginning of the buffer + int i = 0; + for (i = currentPosition-3 ; i >= 0; i--) { + if (mBuffer[i] == '\n') { + // found \n! + if (i > 0 && mBuffer[i-1] == '\r') { + // found \r! + break; + } + } + } + + // here it is either -1 if we reached the start of the buffer without finding + // a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2 + if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') { + // found error! + return true; + } + + return false; + } + + /** + * Returns true if the last line of the result does not start with KO + */ + private boolean isValid(String[] result) { + if (result != null && result.length > 0) { + return !(RE_KO.matcher(result[result.length-1]).matches()); + } + return false; + } + + private int getLatencyIndex(String value) { + try { + // get the int value + int latency = Integer.parseInt(value); + + // check for the speed from the index + for (int i = 0 ; i < MIN_LATENCIES.length; i++) { + if (MIN_LATENCIES[i] == latency) { + return i; + } + } + } catch (NumberFormatException e) { + // Do nothing, we'll just return -1. + } + + return -1; + } + + private int getSpeedIndex(String value) { + try { + // get the int value + int speed = Integer.parseInt(value); + + // check for the speed from the index + for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) { + if (DOWNLOAD_SPEEDS[i] == speed) { + return i; + } + } + } catch (NumberFormatException e) { + // Do nothing, we'll just return -1. + } + + return -1; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java new file mode 100644 index 000000000..b50cf79d9 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java @@ -0,0 +1,767 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides {@link Device} side file listing service. + *

To get an instance for a known {@link Device}, call {@link Device#getFileListingService()}. + */ +public final class FileListingService { + + /** Pattern to find filenames that match "*.apk" */ + private final static Pattern sApkPattern = + Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + private final static String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$ + + /** Pattern to parse the output of the 'pm -lf' command.
+ * The output format looks like:
+ * /data/app/myapp.apk=com.mypackage.myapp */ + private final static Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$ + + /** Top level data folder. */ + public final static String DIRECTORY_DATA = "data"; //$NON-NLS-1$ + /** Top level sdcard folder. */ + public final static String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$ + /** Top level system folder. */ + public final static String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$ + /** Top level temp folder. */ + public final static String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$ + /** Application folder. */ + public final static String DIRECTORY_APP = "app"; //$NON-NLS-1$ + + private final static String[] sRootLevelApprovedItems = { + DIRECTORY_DATA, + DIRECTORY_SDCARD, + DIRECTORY_SYSTEM, + DIRECTORY_TEMP + }; + + public static final long REFRESH_RATE = 5000L; + /** + * Refresh test has to be slightly lower for precision issue. + */ + static final long REFRESH_TEST = (long)(REFRESH_RATE * .8); + + /** Entry type: File */ + public static final int TYPE_FILE = 0; + /** Entry type: Directory */ + public static final int TYPE_DIRECTORY = 1; + /** Entry type: Directory Link */ + public static final int TYPE_DIRECTORY_LINK = 2; + /** Entry type: Block */ + public static final int TYPE_BLOCK = 3; + /** Entry type: Character */ + public static final int TYPE_CHARACTER = 4; + /** Entry type: Link */ + public static final int TYPE_LINK = 5; + /** Entry type: Socket */ + public static final int TYPE_SOCKET = 6; + /** Entry type: FIFO */ + public static final int TYPE_FIFO = 7; + /** Entry type: Other */ + public static final int TYPE_OTHER = 8; + + /** Device side file separator. */ + public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$ + + private static final String FILE_ROOT = "/"; //$NON-NLS-1$ + + + /** + * Regexp pattern to parse the result from ls. + */ + private static Pattern sLsPattern = Pattern.compile( + "^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+(\\S+)\\s+(\\S+)\\s+([\\d\\s,]*)\\s+(\\d{4}-\\d\\d-\\d\\d)\\s+(\\d\\d:\\d\\d)\\s+(.*)$"); //$NON-NLS-1$ + + private Device mDevice; + private FileEntry mRoot; + + private ArrayList mThreadList = new ArrayList(); + + /** + * Represents an entry in a directory. This can be a file or a directory. + */ + public final static class FileEntry { + /** Pattern to escape filenames for shell command consumption. */ + private final static Pattern sEscapePattern = Pattern.compile( + "([\\\\()*+?\"'#/\\s])"); //$NON-NLS-1$ + + /** + * Comparator object for FileEntry + */ + private static Comparator sEntryComparator = new Comparator() { + public int compare(FileEntry o1, FileEntry o2) { + if (o1 instanceof FileEntry && o2 instanceof FileEntry) { + FileEntry fe1 = (FileEntry)o1; + FileEntry fe2 = (FileEntry)o2; + return fe1.name.compareTo(fe2.name); + } + return 0; + } + }; + + FileEntry parent; + String name; + String info; + String permissions; + String size; + String date; + String time; + String owner; + String group; + int type; + boolean isAppPackage; + + boolean isRoot; + + /** + * Indicates whether the entry content has been fetched yet, or not. + */ + long fetchTime = 0; + + final ArrayList mChildren = new ArrayList(); + + /** + * Creates a new file entry. + * @param parent parent entry or null if entry is root + * @param name name of the entry. + * @param type entry type. Can be one of the following: {@link FileListingService#TYPE_FILE}, + * {@link FileListingService#TYPE_DIRECTORY}, {@link FileListingService#TYPE_OTHER}. + */ + private FileEntry(FileEntry parent, String name, int type, boolean isRoot) { + this.parent = parent; + this.name = name; + this.type = type; + this.isRoot = isRoot; + + checkAppPackageStatus(); + } + + /** + * Returns the name of the entry + */ + public String getName() { + return name; + } + + /** + * Returns the size string of the entry, as returned by ls. + */ + public String getSize() { + return size; + } + + /** + * Returns the size of the entry. + */ + public int getSizeValue() { + return Integer.parseInt(size); + } + + /** + * Returns the date string of the entry, as returned by ls. + */ + public String getDate() { + return date; + } + + /** + * Returns the time string of the entry, as returned by ls. + */ + public String getTime() { + return time; + } + + /** + * Returns the permission string of the entry, as returned by ls. + */ + public String getPermissions() { + return permissions; + } + + /** + * Returns the extra info for the entry. + *

For a link, it will be a description of the link. + *

For an application apk file it will be the application package as returned + * by the Package Manager. + */ + public String getInfo() { + return info; + } + + /** + * Return the full path of the entry. + * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator. + */ + public String getFullPath() { + if (isRoot) { + return FILE_ROOT; + } + StringBuilder pathBuilder = new StringBuilder(); + fillPathBuilder(pathBuilder, false); + + return pathBuilder.toString(); + } + + /** + * Return the fully escaped path of the entry. This path is safe to use in a + * shell command line. + * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator + */ + public String getFullEscapedPath() { + StringBuilder pathBuilder = new StringBuilder(); + fillPathBuilder(pathBuilder, true); + + return pathBuilder.toString(); + } + + /** + * Returns the path as a list of segments. + */ + public String[] getPathSegments() { + ArrayList list = new ArrayList(); + fillPathSegments(list); + + return list.toArray(new String[list.size()]); + } + + /** + * Returns true if the entry is a directory, false otherwise; + */ + public int getType() { + return type; + } + + /** + * Returns if the entry is a folder or a link to a folder. + */ + public boolean isDirectory() { + return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK; + } + + /** + * Returns the parent entry. + */ + public FileEntry getParent() { + return parent; + } + + /** + * Returns the cached children of the entry. This returns the cache created from calling + * FileListingService.getChildren(). + */ + public FileEntry[] getCachedChildren() { + return mChildren.toArray(new FileEntry[mChildren.size()]); + } + + /** + * Returns the child {@link FileEntry} matching the name. + * This uses the cached children list. + * @param name the name of the child to return. + * @return the FileEntry matching the name or null. + */ + public FileEntry findChild(String name) { + for (FileEntry entry : mChildren) { + if (entry.name.equals(name)) { + return entry; + } + } + return null; + } + + /** + * Returns whether the entry is the root. + */ + public boolean isRoot() { + return isRoot; + } + + void addChild(FileEntry child) { + mChildren.add(child); + } + + void setChildren(ArrayList newChildren) { + mChildren.clear(); + mChildren.addAll(newChildren); + } + + boolean needFetch() { + if (fetchTime == 0) { + return true; + } + long current = System.currentTimeMillis(); + if (current-fetchTime > REFRESH_TEST) { + return true; + } + + return false; + } + + /** + * Returns if the entry is a valid application package. + */ + public boolean isApplicationPackage() { + return isAppPackage; + } + + /** + * Returns if the file name is an application package name. + */ + public boolean isAppFileName() { + Matcher m = sApkPattern.matcher(name); + return m.matches(); + } + + /** + * Recursively fills the pathBuilder with the full path + * @param pathBuilder a StringBuilder used to create the path. + * @param escapePath Whether the path need to be escaped for consumption by + * a shell command line. + */ + protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) { + if (isRoot) { + return; + } + + if (parent != null) { + parent.fillPathBuilder(pathBuilder, escapePath); + } + pathBuilder.append(FILE_SEPARATOR); + pathBuilder.append(escapePath ? escape(name) : name); + } + + /** + * Recursively fills the segment list with the full path. + * @param list The list of segments to fill. + */ + protected void fillPathSegments(ArrayList list) { + if (isRoot) { + return; + } + + if (parent != null) { + parent.fillPathSegments(list); + } + + list.add(name); + } + + /** + * Sets the internal app package status flag. This checks whether the entry is in an app + * directory like /data/app or /system/app + */ + private void checkAppPackageStatus() { + isAppPackage = false; + + String[] segments = getPathSegments(); + if (type == TYPE_FILE && segments.length == 3 && isAppFileName()) { + isAppPackage = DIRECTORY_APP.equals(segments[1]) && + (DIRECTORY_SYSTEM.equals(segments[0]) || DIRECTORY_DATA.equals(segments[0])); + } + } + + /** + * Returns an escaped version of the entry name. + * @param entryName + */ + private String escape(String entryName) { + return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$ + } + } + + private class LsReceiver extends MultiLineReceiver { + + private ArrayList mEntryList; + private ArrayList mLinkList; + private FileEntry[] mCurrentChildren; + private FileEntry mParentEntry; + + /** + * Create an ls receiver/parser. + * @param currentChildren The list of current children. To prevent + * collapse during update, reusing the same FileEntry objects for + * files that were already there is paramount. + * @param entryList the list of new children to be filled by the + * receiver. + * @param linkList the list of link path to compute post ls, to figure + * out if the link pointed to a file or to a directory. + */ + public LsReceiver(FileEntry parentEntry, ArrayList entryList, + ArrayList linkList) { + mParentEntry = parentEntry; + mCurrentChildren = parentEntry.getCachedChildren(); + mEntryList = entryList; + mLinkList = linkList; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + // no need to handle empty lines. + if (line.length() == 0) { + continue; + } + + // run the line through the regexp + Matcher m = sLsPattern.matcher(line); + if (m.matches() == false) { + continue; + } + + // get the name + String name = m.group(7); + + // if the parent is root, we only accept selected items + if (mParentEntry.isRoot()) { + boolean found = false; + for (String approved : sRootLevelApprovedItems) { + if (approved.equals(name)) { + found = true; + break; + } + } + + // if it's not in the approved list we skip this entry. + if (found == false) { + continue; + } + } + + // get the rest of the groups + String permissions = m.group(1); + String owner = m.group(2); + String group = m.group(3); + String size = m.group(4); + String date = m.group(5); + String time = m.group(6); + String info = null; + + // and the type + int objectType = TYPE_OTHER; + switch (permissions.charAt(0)) { + case '-' : + objectType = TYPE_FILE; + break; + case 'b' : + objectType = TYPE_BLOCK; + break; + case 'c' : + objectType = TYPE_CHARACTER; + break; + case 'd' : + objectType = TYPE_DIRECTORY; + break; + case 'l' : + objectType = TYPE_LINK; + break; + case 's' : + objectType = TYPE_SOCKET; + break; + case 'p' : + objectType = TYPE_FIFO; + break; + } + + + // now check what we may be linking to + if (objectType == TYPE_LINK) { + String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$ + + // we should have 2 segments + if (segments.length == 2) { + // update the entry name to not contain the link + name = segments[0]; + + // and the link name + info = segments[1]; + + // now get the path to the link + String[] pathSegments = info.split(FILE_SEPARATOR); + if (pathSegments.length == 1) { + // the link is to something in the same directory, + // unless the link is .. + if ("..".equals(pathSegments[0])) { //$NON-NLS-1$ + // set the type and we're done. + objectType = TYPE_DIRECTORY_LINK; + } else { + // either we found the object already + // or we'll find it later. + } + } + } + + // add an arrow in front to specify it's a link. + info = "-> " + info; //$NON-NLS-1$; + } + + // get the entry, either from an existing one, or a new one + FileEntry entry = getExistingEntry(name); + if (entry == null) { + entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */); + } + + // add some misc info + entry.permissions = permissions; + entry.size = size; + entry.date = date; + entry.time = time; + entry.owner = owner; + entry.group = group; + if (objectType == TYPE_LINK) { + entry.info = info; + } + + mEntryList.add(entry); + } + } + + /** + * Queries for an already existing Entry per name + * @param name the name of the entry + * @return the existing FileEntry or null if no entry with a matching + * name exists. + */ + private FileEntry getExistingEntry(String name) { + for (int i = 0 ; i < mCurrentChildren.length; i++) { + FileEntry e = mCurrentChildren[i]; + + // since we're going to "erase" the one we use, we need to + // check that the item is not null. + if (e != null) { + // compare per name, case-sensitive. + if (name.equals(e.name)) { + // erase from the list + mCurrentChildren[i] = null; + + // and return the object + return e; + } + } + } + + // couldn't find any matching object, return null + return null; + } + + public boolean isCancelled() { + return false; + } + + public void finishLinks() { + // TODO Handle links in the listing service + } + } + + /** + * Classes which implement this interface provide a method that deals with asynchronous + * result from ls command on the device. + * + * @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver) + */ + public interface IListingReceiver { + public void setChildren(FileEntry entry, FileEntry[] children); + + public void refreshEntry(FileEntry entry); + } + + /** + * Creates a File Listing Service for a specified {@link Device}. + * @param device The Device the service is connected to. + */ + FileListingService(Device device) { + mDevice = device; + } + + /** + * Returns the root element. + * @return the {@link FileEntry} object representing the root element or + * null if the device is invalid. + */ + public FileEntry getRoot() { + if (mDevice != null) { + if (mRoot == null) { + mRoot = new FileEntry(null /* parent */, "" /* name */, TYPE_DIRECTORY, + true /* isRoot */); + } + + return mRoot; + } + + return null; + } + + /** + * Returns the children of a {@link FileEntry}. + *

+ * This method supports a cache mechanism and synchronous and asynchronous modes. + *

+ * If receiver is null, the device side ls + * command is done synchronously, and the method will return upon completion of the command.
+ * If receiver is non null, the command is launched is a separate + * thread and upon completion, the receiver will be notified of the result. + *

+ * The result for each ls command is cached in the parent + * FileEntry. useCache allows usage of this cache, but only if the + * cache is valid. The cache is valid only for {@link FileListingService#REFRESH_RATE} ms. + * After that a new ls command is always executed. + *

+ * If the cache is valid and useCache == true, the method will always simply + * return the value of the cache, whether a {@link IListingReceiver} has been provided or not. + * + * @param entry The parent entry. + * @param useCache A flag to use the cache or to force a new ls command. + * @param receiver A receiver for asynchronous calls. + * @return The list of children or null for asynchronous calls. + * + * @see FileEntry#getCachedChildren() + */ + public FileEntry[] getChildren(final FileEntry entry, boolean useCache, + final IListingReceiver receiver) { + // first thing we do is check the cache, and if we already have a recent + // enough children list, we just return that. + if (useCache && entry.needFetch() == false) { + return entry.getCachedChildren(); + } + + // if there's no receiver, then this is a synchronous call, and we + // return the result of ls + if (receiver == null) { + doLs(entry); + return entry.getCachedChildren(); + } + + // this is a asynchronous call. + // we launch a thread that will do ls and give the listing + // to the receiver + Thread t = new Thread("ls " + entry.getFullPath()) { //$NON-NLS-1$ + @Override + public void run() { + doLs(entry); + + receiver.setChildren(entry, entry.getCachedChildren()); + + final FileEntry[] children = entry.getCachedChildren(); + if (children.length > 0 && children[0].isApplicationPackage()) { + final HashMap map = new HashMap(); + + for (FileEntry child : children) { + String path = child.getFullPath(); + map.put(path, child); + } + + // call pm. + String command = PM_FULL_LISTING; + try { + mDevice.executeShellCommand(command, new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.length() > 0) { + // get the filepath and package from the line + Matcher m = sPmPattern.matcher(line); + if (m.matches()) { + // get the children with that path + FileEntry entry = map.get(m.group(1)); + if (entry != null) { + entry.info = m.group(2); + receiver.refreshEntry(entry); + } + } + } + } + } + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + // adb failed somehow, we do nothing. + } + } + + + // if another thread is pending, launch it + synchronized (mThreadList) { + // first remove ourselves from the list + mThreadList.remove(this); + + // then launch the next one if applicable. + if (mThreadList.size() > 0) { + Thread t = mThreadList.get(0); + t.start(); + } + } + } + }; + + // we don't want to run multiple ls on the device at the same time, so we + // store the thread in a list and launch it only if there's no other thread running. + // the thread will launch the next one once it's done. + synchronized (mThreadList) { + // add to the list + mThreadList.add(t); + + // if it's the only one, launch it. + if (mThreadList.size() == 1) { + t.start(); + } + } + + // and we return null. + return null; + } + + private void doLs(FileEntry entry) { + // create a list that will receive the list of the entries + ArrayList entryList = new ArrayList(); + + // create a list that will receive the link to compute post ls; + ArrayList linkList = new ArrayList(); + + try { + // create the command + String command = "ls -l " + entry.getFullPath(); //$NON-NLS-1$ + + // create the receiver object that will parse the result from ls + LsReceiver receiver = new LsReceiver(entry, entryList, linkList); + + // call ls. + mDevice.executeShellCommand(command, receiver); + + // finish the process of the receiver to handle links + receiver.finishLinks(); + } catch (IOException e) { + } + + + // at this point we need to refresh the viewer + entry.fetchTime = System.currentTimeMillis(); + + // sort the children and set them as the new children + Collections.sort(entryList, FileEntry.sEntryComparator); + entry.setChildren(entryList); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java new file mode 100644 index 000000000..92933799e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A receiver able to parse the result of the execution of + * {@link #GETPROP_COMMAND} on a device. + */ +final class GetPropReceiver extends MultiLineReceiver { + final static String GETPROP_COMMAND = "getprop"; //$NON-NLS-1$ + + private final static Pattern GETPROP_PATTERN = Pattern.compile("^\\[([^]]+)\\]\\:\\s*\\[(.*)\\]$"); //$NON-NLS-1$ + + /** indicates if we need to read the first */ + private Device mDevice = null; + + /** + * Creates the receiver with the device the receiver will modify. + * @param device The device to modify + */ + public GetPropReceiver(Device device) { + mDevice = device; + } + + @Override + public void processNewLines(String[] lines) { + // We receive an array of lines. We're expecting + // to have the build info in the first line, and the build + // date in the 2nd line. There seems to be an empty line + // after all that. + + for (String line : lines) { + if (line.length() == 0 || line.startsWith("#")) { + continue; + } + + Matcher m = GETPROP_PATTERN.matcher(line); + if (m.matches()) { + String label = m.group(1); + String value = m.group(2); + + if (label.length() > 0) { + mDevice.addProperty(label, value); + } + } + } + } + + public boolean isCancelled() { + return false; + } + + @Override + public void done() { + mDevice.update(Device.CHANGE_BUILD_INFO); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java new file mode 100644 index 000000000..c821dfcdc --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle the "app name" chunk (APNM). + */ +final class HandleAppName extends ChunkHandler { + + public static final int CHUNK_APNM = ChunkHandler.type("APNM"); + + private static final HandleAppName mInst = new HandleAppName(); + + + private HandleAppName() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_APNM, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, + boolean isReply, int msgId) { + + Log.d("ddm-appname", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_APNM) { + assert !isReply; + handleAPNM(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our APNM message. + */ + private static void handleAPNM(Client client, ByteBuffer data) { + int appNameLen; + String appName; + + appNameLen = data.getInt(); + appName = getString(data, appNameLen); + + Log.d("ddm-appname", "APNM: app='" + appName + "'"); + + ClientData cd = client.getClientData(); + synchronized (cd) { + cd.setClientDescription(appName); + } + + client = checkDebuggerPortForAppName(client, appName); + + if (client != null) { + client.update(Client.CHANGE_NAME); + } + } + } + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java new file mode 100644 index 000000000..adeedbba4 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Submit an exit request. + */ +final class HandleExit extends ChunkHandler { + + public static final int CHUNK_EXIT = type("EXIT"); + + private static final HandleExit mInst = new HandleExit(); + + + private HandleExit() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) {} + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + handleUnknownChunk(client, type, data, isReply, msgId); + } + + /** + * Send an EXIT request to the client. + */ + public static void sendEXIT(Client client, int status) + throws IOException + { + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(status); + + finishChunkPacket(packet, CHUNK_EXIT, buf.position()); + Log.d("ddm-exit", "Sending " + name(CHUNK_EXIT) + ": " + status); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java new file mode 100644 index 000000000..e5b403b30 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java @@ -0,0 +1,597 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.ClientData.AllocationTrackingStatus; +import com.android.ddmlib.ClientData.IHprofDumpHandler; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; + +/** + * Handle heap status updates. + */ +final class HandleHeap extends ChunkHandler { + + public static final int CHUNK_HPIF = type("HPIF"); + public static final int CHUNK_HPST = type("HPST"); + public static final int CHUNK_HPEN = type("HPEN"); + public static final int CHUNK_HPSG = type("HPSG"); + public static final int CHUNK_HPGC = type("HPGC"); + public static final int CHUNK_HPDU = type("HPDU"); + public static final int CHUNK_HPDS = type("HPDS"); + public static final int CHUNK_REAE = type("REAE"); + public static final int CHUNK_REAQ = type("REAQ"); + public static final int CHUNK_REAL = type("REAL"); + + // args to sendHPSG + public static final int WHEN_DISABLE = 0; + public static final int WHEN_GC = 1; + public static final int WHAT_MERGE = 0; // merge adjacent objects + public static final int WHAT_OBJ = 1; // keep objects distinct + + // args to sendHPIF + public static final int HPIF_WHEN_NEVER = 0; + public static final int HPIF_WHEN_NOW = 1; + public static final int HPIF_WHEN_NEXT_GC = 2; + public static final int HPIF_WHEN_EVERY_GC = 3; + + private static final HandleHeap mInst = new HandleHeap(); + + private HandleHeap() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_HPIF, mInst); + mt.registerChunkHandler(CHUNK_HPST, mInst); + mt.registerChunkHandler(CHUNK_HPEN, mInst); + mt.registerChunkHandler(CHUNK_HPSG, mInst); + mt.registerChunkHandler(CHUNK_HPDS, mInst); + mt.registerChunkHandler(CHUNK_REAQ, mInst); + mt.registerChunkHandler(CHUNK_REAL, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + if (client.isHeapUpdateEnabled()) { + //sendHPSG(client, WHEN_GC, WHAT_MERGE); + sendHPIF(client, HPIF_WHEN_EVERY_GC); + } + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + Log.d("ddm-heap", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_HPIF) { + handleHPIF(client, data); + } else if (type == CHUNK_HPST) { + handleHPST(client, data); + } else if (type == CHUNK_HPEN) { + handleHPEN(client, data); + } else if (type == CHUNK_HPSG) { + handleHPSG(client, data); + } else if (type == CHUNK_HPDU) { + handleHPDU(client, data); + } else if (type == CHUNK_HPDS) { + handleHPDS(client, data); + } else if (type == CHUNK_REAQ) { + handleREAQ(client, data); + } else if (type == CHUNK_REAL) { + handleREAL(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a heap info message. + */ + private void handleHPIF(Client client, ByteBuffer data) { + Log.d("ddm-heap", "HPIF!"); + try { + int numHeaps = data.getInt(); + + for (int i = 0; i < numHeaps; i++) { + int heapId = data.getInt(); + @SuppressWarnings("unused") + long timeStamp = data.getLong(); + @SuppressWarnings("unused") + byte reason = data.get(); + long maxHeapSize = (long)data.getInt() & 0x00ffffffff; + long heapSize = (long)data.getInt() & 0x00ffffffff; + long bytesAllocated = (long)data.getInt() & 0x00ffffffff; + long objectsAllocated = (long)data.getInt() & 0x00ffffffff; + + client.getClientData().setHeapInfo(heapId, maxHeapSize, + heapSize, bytesAllocated, objectsAllocated); + client.update(Client.CHANGE_HEAP_DATA); + } + } catch (BufferUnderflowException ex) { + Log.w("ddm-heap", "malformed HPIF chunk from client"); + } + } + + /** + * Send an HPIF (HeaP InFo) request to the client. + */ + public static void sendHPIF(Client client, int when) throws IOException { + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte)when); + + finishChunkPacket(packet, CHUNK_HPIF, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPIF) + ": when=" + when); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle a heap segment series start message. + */ + private void handleHPST(Client client, ByteBuffer data) { + /* Clear out any data that's sitting around to + * get ready for the chunks that are about to come. + */ +//xxx todo: only clear data that belongs to the heap mentioned in . + client.getClientData().getVmHeapData().clearHeapData(); + } + + /* + * Handle a heap segment series end message. + */ + private void handleHPEN(Client client, ByteBuffer data) { + /* Let the UI know that we've received all of the + * data for this heap. + */ +//xxx todo: only seal data that belongs to the heap mentioned in . + client.getClientData().getVmHeapData().sealHeapData(); + client.update(Client.CHANGE_HEAP_DATA); + } + + /* + * Handle a heap segment message. + */ + private void handleHPSG(Client client, ByteBuffer data) { + byte dataCopy[] = new byte[data.limit()]; + data.rewind(); + data.get(dataCopy); + data = ByteBuffer.wrap(dataCopy); + client.getClientData().getVmHeapData().addHeapData(data); +//xxx todo: add to the heap mentioned in + } + + /** + * Sends an HPSG (HeaP SeGment) request to the client. + */ + public static void sendHPSG(Client client, int when, int what) + throws IOException { + + ByteBuffer rawBuf = allocBuffer(2); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte)when); + buf.put((byte)what); + + finishChunkPacket(packet, CHUNK_HPSG, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPSG) + ": when=" + + when + ", what=" + what); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends an HPGC request to the client. + */ + public static void sendHPGC(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_HPGC, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPGC)); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends an HPDU request to the client. + * + * We will get an HPDU response when the heap dump has completed. On + * failure we get a generic failure response. + * + * @param fileName name of output file (on device) + */ + public static void sendHPDU(Client client, String fileName) + throws IOException { + ByteBuffer rawBuf = allocBuffer(4 + fileName.length() * 2); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(fileName.length()); + putString(buf, fileName); + + finishChunkPacket(packet, CHUNK_HPDU, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPDU) + " '" + fileName +"'"); + client.sendAndConsume(packet, mInst); + client.getClientData().setPendingHprofDump(fileName); + } + + /** + * Sends an HPDS request to the client. + * + * We will get an HPDS response when the heap dump has completed. On + * failure we get a generic failure response. + * + * This is more expensive for the device than HPDU, because the entire + * heap dump is held in RAM instead of spooled out to a temp file. On + * the other hand, permission to write to /sdcard is not required. + * + * @param fileName name of output file (on device) + */ + public static void sendHPDS(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + finishChunkPacket(packet, CHUNK_HPDS, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_HPDS)); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle notification of completion of a HeaP DUmp. + */ + private void handleHPDU(Client client, ByteBuffer data) { + byte result; + + // get the filename and make the client not have pending HPROF dump anymore. + String filename = client.getClientData().getPendingHprofDump(); + client.getClientData().setPendingHprofDump(null); + + // get the dump result + result = data.get(); + + // get the app-level handler for HPROF dump + IHprofDumpHandler handler = ClientData.getHprofDumpHandler(); + if (handler != null) { + if (result == 0) { + handler.onSuccess(filename, client); + + Log.d("ddm-heap", "Heap dump request has finished"); + } else { + handler.onEndFailure(client, null); + Log.w("ddm-heap", "Heap dump request failed (check device log)"); + } + } + } + + /* + * Handle HeaP Dump Streaming response. "data" contains the full + * hprof dump. + */ + private void handleHPDS(Client client, ByteBuffer data) { + IHprofDumpHandler handler = ClientData.getHprofDumpHandler(); + if (handler != null) { + byte[] stuff = new byte[data.capacity()]; + data.get(stuff, 0, stuff.length); + + Log.d("ddm-hprof", "got hprof file, size: " + data.capacity() + " bytes"); + + handler.onSuccess(stuff, client); + } + } + + /** + * Sends a REAE (REcent Allocation Enable) request to the client. + */ + public static void sendREAE(Client client, boolean enable) + throws IOException { + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.put((byte) (enable ? 1 : 0)); + + finishChunkPacket(packet, CHUNK_REAE, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAE) + ": " + enable); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends a REAQ (REcent Allocation Query) request to the client. + */ + public static void sendREAQ(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_REAQ, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAQ)); + client.sendAndConsume(packet, mInst); + } + + /** + * Sends a REAL (REcent ALlocation) request to the client. + */ + public static void sendREAL(Client client) + throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_REAL, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_REAL)); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle the response from our REcent Allocation Query message. + */ + private void handleREAQ(Client client, ByteBuffer data) { + boolean enabled; + + enabled = (data.get() != 0); + Log.d("ddm-heap", "REAQ says: enabled=" + enabled); + + client.getClientData().setAllocationStatus(enabled ? + AllocationTrackingStatus.ON : AllocationTrackingStatus.OFF); + client.update(Client.CHANGE_HEAP_ALLOCATION_STATUS); + } + + /** + * Converts a VM class descriptor string ("Landroid/os/Debug;") to + * a dot-notation class name ("android.os.Debug"). + */ + private String descriptorToDot(String str) { + // count the number of arrays. + int array = 0; + while (str.startsWith("[")) { + str = str.substring(1); + array++; + } + + int len = str.length(); + + /* strip off leading 'L' and trailing ';' if appropriate */ + if (len >= 2 && str.charAt(0) == 'L' && str.charAt(len - 1) == ';') { + str = str.substring(1, len-1); + str = str.replace('/', '.'); + } else { + // convert the basic types + if ("C".equals(str)) { + str = "char"; + } else if ("B".equals(str)) { + str = "byte"; + } else if ("Z".equals(str)) { + str = "boolean"; + } else if ("S".equals(str)) { + str = "short"; + } else if ("I".equals(str)) { + str = "int"; + } else if ("J".equals(str)) { + str = "long"; + } else if ("F".equals(str)) { + str = "float"; + } else if ("D".equals(str)) { + str = "double"; + } + } + + // now add the array part + for (int a = 0 ; a < array; a++) { + str = str + "[]"; + } + + return str; + } + + /** + * Reads a string table out of "data". + * + * This is just a serial collection of strings, each of which is a + * four-byte length followed by UTF-16 data. + */ + private void readStringTable(ByteBuffer data, String[] strings) { + int count = strings.length; + int i; + + for (i = 0; i < count; i++) { + int nameLen = data.getInt(); + String descriptor = getString(data, nameLen); + strings[i] = descriptorToDot(descriptor); + } + } + + /* + * Handle a REcent ALlocation response. + * + * Message header (all values big-endian): + * (1b) message header len (to allow future expansion); includes itself + * (1b) entry header len + * (1b) stack frame len + * (2b) number of entries + * (4b) offset to string table from start of message + * (2b) number of class name strings + * (2b) number of method name strings + * (2b) number of source file name strings + * For each entry: + * (4b) total allocation size + * (2b) threadId + * (2b) allocated object's class name index + * (1b) stack depth + * For each stack frame: + * (2b) method's class name + * (2b) method name + * (2b) method source file + * (2b) line number, clipped to 32767; -2 if native; -1 if no source + * (xb) class name strings + * (xb) method name strings + * (xb) source file strings + * + * As with other DDM traffic, strings are sent as a 4-byte length + * followed by UTF-16 data. + */ + private void handleREAL(Client client, ByteBuffer data) { + Log.e("ddm-heap", "*** Received " + name(CHUNK_REAL)); + int messageHdrLen, entryHdrLen, stackFrameLen; + int numEntries, offsetToStrings; + int numClassNames, numMethodNames, numFileNames; + + /* + * Read the header. + */ + messageHdrLen = (data.get() & 0xff); + entryHdrLen = (data.get() & 0xff); + stackFrameLen = (data.get() & 0xff); + numEntries = (data.getShort() & 0xffff); + offsetToStrings = data.getInt(); + numClassNames = (data.getShort() & 0xffff); + numMethodNames = (data.getShort() & 0xffff); + numFileNames = (data.getShort() & 0xffff); + + + /* + * Skip forward to the strings and read them. + */ + data.position(offsetToStrings); + + String[] classNames = new String[numClassNames]; + String[] methodNames = new String[numMethodNames]; + String[] fileNames = new String[numFileNames]; + + readStringTable(data, classNames); + readStringTable(data, methodNames); + //System.out.println("METHODS: " + // + java.util.Arrays.deepToString(methodNames)); + readStringTable(data, fileNames); + + /* + * Skip back to a point just past the header and start reading + * entries. + */ + data.position(messageHdrLen); + + ArrayList list = new ArrayList(numEntries); + for (int i = 0; i < numEntries; i++) { + int totalSize; + int threadId, classNameIndex, stackDepth; + + totalSize = data.getInt(); + threadId = (data.getShort() & 0xffff); + classNameIndex = (data.getShort() & 0xffff); + stackDepth = (data.get() & 0xff); + /* we've consumed 9 bytes; gobble up any extra */ + for (int skip = 9; skip < entryHdrLen; skip++) + data.get(); + + StackTraceElement[] steArray = new StackTraceElement[stackDepth]; + + /* + * Pull out the stack trace. + */ + for (int sti = 0; sti < stackDepth; sti++) { + int methodClassNameIndex, methodNameIndex; + int methodSourceFileIndex; + short lineNumber; + String methodClassName, methodName, methodSourceFile; + + methodClassNameIndex = (data.getShort() & 0xffff); + methodNameIndex = (data.getShort() & 0xffff); + methodSourceFileIndex = (data.getShort() & 0xffff); + lineNumber = data.getShort(); + + methodClassName = classNames[methodClassNameIndex]; + methodName = methodNames[methodNameIndex]; + methodSourceFile = fileNames[methodSourceFileIndex]; + + steArray[sti] = new StackTraceElement(methodClassName, + methodName, methodSourceFile, lineNumber); + + /* we've consumed 8 bytes; gobble up any extra */ + for (int skip = 9; skip < stackFrameLen; skip++) + data.get(); + } + + list.add(new AllocationInfo(classNames[classNameIndex], + totalSize, (short) threadId, steArray)); + } + + // sort biggest allocations first. + Collections.sort(list); + + client.getClientData().setAllocations(list.toArray(new AllocationInfo[numEntries])); + client.update(Client.CHANGE_HEAP_ALLOCATIONS); + } + + /* + * For debugging: dump the contents of an AllocRecord array. + * + * The array starts with the oldest known allocation and ends with + * the most recent allocation. + */ + @SuppressWarnings("unused") + private static void dumpRecords(AllocationInfo[] records) { + System.out.println("Found " + records.length + " records:"); + + for (AllocationInfo rec: records) { + System.out.println("tid=" + rec.getThreadId() + " " + + rec.getAllocatedClass() + " (" + rec.getSize() + " bytes)"); + + for (StackTraceElement ste: rec.getStackTrace()) { + if (ste.isNativeMethod()) { + System.out.println(" " + ste.getClassName() + + "." + ste.getMethodName() + + " (Native method)"); + } else { + System.out.println(" " + ste.getClassName() + + "." + ste.getMethodName() + + " (" + ste.getFileName() + + ":" + ste.getLineNumber() + ")"); + } + } + } + } + +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java new file mode 100644 index 000000000..4e62bca29 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle the "hello" chunk (HELO) and feature discovery. + */ +final class HandleHello extends ChunkHandler { + + public static final int CHUNK_HELO = ChunkHandler.type("HELO"); + public static final int CHUNK_FEAT = ChunkHandler.type("FEAT"); + + private static final HandleHello mInst = new HandleHello(); + + private HandleHello() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_HELO, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + Log.d("ddm-hello", "Now ready: " + client); + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) { + Log.d("ddm-hello", "Now disconnected: " + client); + } + + /** + * Sends HELLO-type commands to the VM after a good handshake. + * @param client + * @param serverProtocolVersion + * @throws IOException + */ + public static void sendHelloCommands(Client client, int serverProtocolVersion) + throws IOException { + sendHELO(client, serverProtocolVersion); + sendFEAT(client); + HandleProfiling.sendMPRQ(client); + } + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-hello", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_HELO) { + assert isReply; + handleHELO(client, data); + } else if (type == CHUNK_FEAT) { + handleFEAT(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our HELO message. + */ + private static void handleHELO(Client client, ByteBuffer data) { + int version, pid, vmIdentLen, appNameLen; + String vmIdent, appName; + + version = data.getInt(); + pid = data.getInt(); + vmIdentLen = data.getInt(); + appNameLen = data.getInt(); + + vmIdent = getString(data, vmIdentLen); + appName = getString(data, appNameLen); + + Log.d("ddm-hello", "HELO: v=" + version + ", pid=" + pid + + ", vm='" + vmIdent + "', app='" + appName + "'"); + + ClientData cd = client.getClientData(); + + synchronized (cd) { + if (cd.getPid() == pid) { + cd.setVmIdentifier(vmIdent); + cd.setClientDescription(appName); + cd.isDdmAware(true); + } else { + Log.e("ddm-hello", "Received pid (" + pid + ") does not match client pid (" + + cd.getPid() + ")"); + } + } + + client = checkDebuggerPortForAppName(client, appName); + + if (client != null) { + client.update(Client.CHANGE_NAME); + } + } + + + /** + * Send a HELO request to the client. + */ + public static void sendHELO(Client client, int serverProtocolVersion) + throws IOException + { + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(serverProtocolVersion); + + finishChunkPacket(packet, CHUNK_HELO, buf.position()); + Log.d("ddm-hello", "Sending " + name(CHUNK_HELO) + + " ID=0x" + Integer.toHexString(packet.getId())); + client.sendAndConsume(packet, mInst); + } + + /** + * Handle a reply to our FEAT request. + */ + private static void handleFEAT(Client client, ByteBuffer data) { + int featureCount; + int i; + + featureCount = data.getInt(); + for (i = 0; i < featureCount; i++) { + int len = data.getInt(); + String feature = getString(data, len); + client.getClientData().addFeature(feature); + + Log.d("ddm-hello", "Feature: " + feature); + } + } + + /** + * Send a FEAT request to the client. + */ + public static void sendFEAT(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_FEAT, buf.position()); + Log.d("ddm-heap", "Sending " + name(CHUNK_FEAT)); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java new file mode 100644 index 000000000..ca2659066 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Handle thread status updates. + */ +final class HandleNativeHeap extends ChunkHandler { + + public static final int CHUNK_NHGT = type("NHGT"); // $NON-NLS-1$ + public static final int CHUNK_NHSG = type("NHSG"); // $NON-NLS-1$ + public static final int CHUNK_NHST = type("NHST"); // $NON-NLS-1$ + public static final int CHUNK_NHEN = type("NHEN"); // $NON-NLS-1$ + + private static final HandleNativeHeap mInst = new HandleNativeHeap(); + + private HandleNativeHeap() { + } + + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_NHGT, mInst); + mt.registerChunkHandler(CHUNK_NHSG, mInst); + mt.registerChunkHandler(CHUNK_NHST, mInst); + mt.registerChunkHandler(CHUNK_NHEN, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-nativeheap", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_NHGT) { + handleNHGT(client, data); + } else if (type == CHUNK_NHST) { + // start chunk before any NHSG chunk(s) + client.getClientData().getNativeHeapData().clearHeapData(); + } else if (type == CHUNK_NHEN) { + // end chunk after NHSG chunk(s) + client.getClientData().getNativeHeapData().sealHeapData(); + } else if (type == CHUNK_NHSG) { + handleNHSG(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + + client.update(Client.CHANGE_NATIVE_HEAP_DATA); + } + + /** + * Send an NHGT (Native Thread GeT) request to the client. + */ + public static void sendNHGT(Client client) throws IOException { + + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data in request message + + finishChunkPacket(packet, CHUNK_NHGT, buf.position()); + Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHGT)); + client.sendAndConsume(packet, mInst); + + rawBuf = allocBuffer(2); + packet = new JdwpPacket(rawBuf); + buf = getChunkDataBuf(rawBuf); + + buf.put((byte)HandleHeap.WHEN_GC); + buf.put((byte)HandleHeap.WHAT_OBJ); + + finishChunkPacket(packet, CHUNK_NHSG, buf.position()); + Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHSG)); + client.sendAndConsume(packet, mInst); + } + + /* + * Handle our native heap data. + */ + private void handleNHGT(Client client, ByteBuffer data) { + ClientData cd = client.getClientData(); + + Log.d("ddm-nativeheap", "NHGT: " + data.limit() + " bytes"); + + // TODO - process incoming data and save in "cd" + byte[] copy = new byte[data.limit()]; + data.get(copy); + + // clear the previous run + cd.clearNativeAllocationInfo(); + + ByteBuffer buffer = ByteBuffer.wrap(copy); + buffer.order(ByteOrder.LITTLE_ENDIAN); + +// read the header +// typedef struct Header { +// uint32_t mapSize; +// uint32_t allocSize; +// uint32_t allocInfoSize; +// uint32_t totalMemory; +// uint32_t backtraceSize; +// }; + + int mapSize = buffer.getInt(); + int allocSize = buffer.getInt(); + int allocInfoSize = buffer.getInt(); + int totalMemory = buffer.getInt(); + int backtraceSize = buffer.getInt(); + + Log.d("ddms", "mapSize: " + mapSize); + Log.d("ddms", "allocSize: " + allocSize); + Log.d("ddms", "allocInfoSize: " + allocInfoSize); + Log.d("ddms", "totalMemory: " + totalMemory); + + cd.setTotalNativeMemory(totalMemory); + + // this means that updates aren't turned on. + if (allocInfoSize == 0) + return; + + if (mapSize > 0) { + byte[] maps = new byte[mapSize]; + buffer.get(maps, 0, mapSize); + parseMaps(cd, maps); + } + + int iterations = allocSize / allocInfoSize; + + for (int i = 0 ; i < iterations ; i++) { + NativeAllocationInfo info = new NativeAllocationInfo( + buffer.getInt() /* size */, + buffer.getInt() /* allocations */); + + for (int j = 0 ; j < backtraceSize ; j++) { + long addr = ((long)buffer.getInt()) & 0x00000000ffffffffL; + + info.addStackCallAddress(addr);; + } + + cd.addNativeAllocation(info); + } + } + + private void handleNHSG(Client client, ByteBuffer data) { + byte dataCopy[] = new byte[data.limit()]; + data.rewind(); + data.get(dataCopy); + data = ByteBuffer.wrap(dataCopy); + client.getClientData().getNativeHeapData().addHeapData(data); + + if (true) { + return; + } + + // WORK IN PROGRESS + +// Log.e("ddm-nativeheap", "NHSG: ----------------------------------"); +// Log.e("ddm-nativeheap", "NHSG: " + data.limit() + " bytes"); + + byte[] copy = new byte[data.limit()]; + data.get(copy); + + ByteBuffer buffer = ByteBuffer.wrap(copy); + buffer.order(ByteOrder.BIG_ENDIAN); + + int id = buffer.getInt(); + int unitsize = (int) buffer.get(); + long startAddress = (long) buffer.getInt() & 0x00000000ffffffffL; + int offset = buffer.getInt(); + int allocationUnitCount = buffer.getInt(); + +// Log.e("ddm-nativeheap", "id: " + id); +// Log.e("ddm-nativeheap", "unitsize: " + unitsize); +// Log.e("ddm-nativeheap", "startAddress: 0x" + Long.toHexString(startAddress)); +// Log.e("ddm-nativeheap", "offset: " + offset); +// Log.e("ddm-nativeheap", "allocationUnitCount: " + allocationUnitCount); +// Log.e("ddm-nativeheap", "end: 0x" + +// Long.toHexString(startAddress + unitsize * allocationUnitCount)); + + // read the usage + while (buffer.position() < buffer.limit()) { + int eState = (int)buffer.get() & 0x000000ff; + int eLen = ((int)buffer.get() & 0x000000ff) + 1; + //Log.e("ddm-nativeheap", "solidity: " + (eState & 0x7) + " - kind: " + // + ((eState >> 3) & 0x7) + " - len: " + eLen); + } + + +// count += unitsize * allocationUnitCount; +// Log.e("ddm-nativeheap", "count = " + count); + + } + + private void parseMaps(ClientData cd, byte[] maps) { + InputStreamReader input = new InputStreamReader(new ByteArrayInputStream(maps)); + BufferedReader reader = new BufferedReader(input); + + String line; + + try { + + // most libraries are defined on several lines, so we need to make sure we parse + // all the library lines and only add the library at the end + long startAddr = 0; + long endAddr = 0; + String library = null; + + while ((line = reader.readLine()) != null) { + Log.d("ddms", "line: " + line); + if (line.length() < 16) { + continue; + } + + try { + long tmpStart = Long.parseLong(line.substring(0, 8), 16); + long tmpEnd = Long.parseLong(line.substring(9, 17), 16); + + /* + * only check for library addresses as defined in + * //device/config/prelink-linux-arm.map + */ + if (tmpStart >= 0x0000000080000000L && tmpStart <= 0x00000000BFFFFFFFL) { + + int index = line.indexOf('/'); + + if (index == -1) + continue; + + String tmpLib = line.substring(index); + + if (library == null || + (library != null && tmpLib.equals(library) == false)) { + + if (library != null) { + cd.addNativeLibraryMapInfo(startAddr, endAddr, library); + Log.d("ddms", library + "(" + Long.toHexString(startAddr) + + " - " + Long.toHexString(endAddr) + ")"); + } + + // now init the new library + library = tmpLib; + startAddr = tmpStart; + endAddr = tmpEnd; + } else { + // add the new end + endAddr = tmpEnd; + } + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + if (library != null) { + cd.addNativeLibraryMapInfo(startAddr, endAddr, library); + Log.d("ddms", library + "(" + Long.toHexString(startAddr) + + " - " + Long.toHexString(endAddr) + ")"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleProfiling.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleProfiling.java new file mode 100644 index 000000000..059526708 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleProfiling.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2009 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.ddmlib; + +import com.android.ddmlib.ClientData.IMethodProfilingHandler; +import com.android.ddmlib.ClientData.MethodProfilingStatus; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle heap status updates. + */ +final class HandleProfiling extends ChunkHandler { + + public static final int CHUNK_MPRS = type("MPRS"); + public static final int CHUNK_MPRE = type("MPRE"); + public static final int CHUNK_MPSS = type("MPSS"); + public static final int CHUNK_MPSE = type("MPSE"); + public static final int CHUNK_MPRQ = type("MPRQ"); + public static final int CHUNK_FAIL = type("FAIL"); + + private static final HandleProfiling mInst = new HandleProfiling(); + + private HandleProfiling() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_MPRE, mInst); + mt.registerChunkHandler(CHUNK_MPSE, mInst); + mt.registerChunkHandler(CHUNK_MPRQ, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, + boolean isReply, int msgId) { + + Log.d("ddm-prof", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_MPRE) { + handleMPRE(client, data); + } else if (type == CHUNK_MPSE) { + handleMPSE(client, data); + } else if (type == CHUNK_MPRQ) { + handleMPRQ(client, data); + } else if (type == CHUNK_FAIL) { + handleFAIL(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /** + * Send a MPRS (Method PRofiling Start) request to the client. + * + * The arguments to this method will eventually be passed to + * android.os.Debug.startMethodTracing() on the device. + * + * @param fileName is the name of the file to which profiling data + * will be written (on the device); it will have ".trace" + * appended if necessary + * @param bufferSize is the desired buffer size in bytes (8MB is good) + * @param flags see startMethodTracing() docs; use 0 for default behavior + */ + public static void sendMPRS(Client client, String fileName, int bufferSize, + int flags) throws IOException { + + ByteBuffer rawBuf = allocBuffer(3*4 + fileName.length() * 2); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(bufferSize); + buf.putInt(flags); + buf.putInt(fileName.length()); + putString(buf, fileName); + + finishChunkPacket(packet, CHUNK_MPRS, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPRS) + " '" + fileName + + "', size=" + bufferSize + ", flags=" + flags); + client.sendAndConsume(packet, mInst); + + // record the filename we asked for. + client.getClientData().setPendingMethodProfiling(fileName); + + // send a status query. this ensure that the status is properly updated if for some + // reason starting the tracing failed. + sendMPRQ(client); + } + + /** + * Send a MPRE (Method PRofiling End) request to the client. + */ + public static void sendMPRE(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_MPRE, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPRE)); + client.sendAndConsume(packet, mInst); + } + + /** + * Handle notification that method profiling has finished writing + * data to disk. + */ + private void handleMPRE(Client client, ByteBuffer data) { + byte result; + + // get the filename and make the client not have pending HPROF dump anymore. + String filename = client.getClientData().getPendingMethodProfiling(); + client.getClientData().setPendingMethodProfiling(null); + + result = data.get(); + + // get the app-level handler for method tracing dump + IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler(); + if (handler != null) { + if (result == 0) { + handler.onSuccess(filename, client); + + Log.d("ddm-prof", "Method profiling has finished"); + } else { + handler.onEndFailure(client, null /*message*/); + + Log.w("ddm-prof", "Method profiling has failed (check device log)"); + } + } + + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF); + client.update(Client.CHANGE_METHOD_PROFILING_STATUS); + } + + /** + * Send a MPSS (Method Profiling Streaming Start) request to the client. + * + * The arguments to this method will eventually be passed to + * android.os.Debug.startMethodTracing() on the device. + * + * @param bufferSize is the desired buffer size in bytes (8MB is good) + * @param flags see startMethodTracing() docs; use 0 for default behavior + */ + public static void sendMPSS(Client client, int bufferSize, + int flags) throws IOException { + + ByteBuffer rawBuf = allocBuffer(2*4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(bufferSize); + buf.putInt(flags); + + finishChunkPacket(packet, CHUNK_MPSS, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPSS) + + "', size=" + bufferSize + ", flags=" + flags); + client.sendAndConsume(packet, mInst); + + // send a status query. this ensure that the status is properly updated if for some + // reason starting the tracing failed. + sendMPRQ(client); + } + + /** + * Send a MPSE (Method Profiling Streaming End) request to the client. + */ + public static void sendMPSE(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_MPSE, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPSE)); + client.sendAndConsume(packet, mInst); + } + + /** + * Handle incoming profiling data. The MPSE packet includes the + * complete .trace file. + */ + private void handleMPSE(Client client, ByteBuffer data) { + IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler(); + if (handler != null) { + byte[] stuff = new byte[data.capacity()]; + data.get(stuff, 0, stuff.length); + + Log.d("ddm-prof", "got trace file, size: " + stuff.length + " bytes"); + + handler.onSuccess(stuff, client); + } + + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF); + client.update(Client.CHANGE_METHOD_PROFILING_STATUS); + } + + /** + * Send a MPRQ (Method PRofiling Query) request to the client. + */ + public static void sendMPRQ(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // no data + + finishChunkPacket(packet, CHUNK_MPRQ, buf.position()); + Log.d("ddm-prof", "Sending " + name(CHUNK_MPRQ)); + client.sendAndConsume(packet, mInst); + } + + /** + * Receive response to query. + */ + private void handleMPRQ(Client client, ByteBuffer data) { + byte result; + + result = data.get(); + + if (result == 0) { + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF); + Log.d("ddm-prof", "Method profiling is not running"); + } else { + client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.ON); + Log.d("ddm-prof", "Method profiling is running"); + } + client.update(Client.CHANGE_METHOD_PROFILING_STATUS); + } + + private void handleFAIL(Client client, ByteBuffer data) { + /*int errorCode =*/ data.getInt(); + int length = data.getInt() * 2; + String message = null; + if (length > 0) { + byte[] messageBuffer = new byte[length]; + data.get(messageBuffer, 0, length); + message = new String(messageBuffer); + } + + // this can be sent if + // - MPRS failed (like wrong permission) + // - MPSE failed for whatever reason + + String filename = client.getClientData().getPendingMethodProfiling(); + if (filename != null) { + // reset the pending file. + client.getClientData().setPendingMethodProfiling(null); + + // and notify of failure + IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler(); + if (handler != null) { + handler.onStartFailure(client, message); + } + } else { + // this is MPRE + // notify of failure + IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler(); + if (handler != null) { + handler.onEndFailure(client, message); + } + } + + // send a query to know the current status + try { + sendMPRQ(client); + } catch (IOException e) { + Log.e("HandleProfiling", e); + } + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java new file mode 100644 index 000000000..b9f3a7485 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.Log.LogLevel; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle thread status updates. + */ +final class HandleTest extends ChunkHandler { + + public static final int CHUNK_TEST = type("TEST"); + + private static final HandleTest mInst = new HandleTest(); + + + private HandleTest() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_TEST, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-test", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_TEST) { + handleTEST(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a thread creation message. + */ + private void handleTEST(Client client, ByteBuffer data) + { + /* + * Can't call data.array() on a read-only ByteBuffer, so we make + * a copy. + */ + byte[] copy = new byte[data.limit()]; + data.get(copy); + + Log.d("ddm-test", "Received:"); + Log.hexDump("ddm-test", LogLevel.DEBUG, copy, 0, copy.length); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java new file mode 100644 index 000000000..8430c95d6 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle thread status updates. + */ +final class HandleThread extends ChunkHandler { + + public static final int CHUNK_THEN = type("THEN"); + public static final int CHUNK_THCR = type("THCR"); + public static final int CHUNK_THDE = type("THDE"); + public static final int CHUNK_THST = type("THST"); + public static final int CHUNK_THNM = type("THNM"); + public static final int CHUNK_STKL = type("STKL"); + + private static final HandleThread mInst = new HandleThread(); + + // only read/written by requestThreadUpdates() + private static volatile boolean mThreadStatusReqRunning = false; + private static volatile boolean mThreadStackTraceReqRunning = false; + + private HandleThread() {} + + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_THCR, mInst); + mt.registerChunkHandler(CHUNK_THDE, mInst); + mt.registerChunkHandler(CHUNK_THST, mInst); + mt.registerChunkHandler(CHUNK_THNM, mInst); + mt.registerChunkHandler(CHUNK_STKL, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException { + Log.d("ddm-thread", "Now ready: " + client); + if (client.isThreadUpdateEnabled()) + sendTHEN(client, true); + } + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-thread", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_THCR) { + handleTHCR(client, data); + } else if (type == CHUNK_THDE) { + handleTHDE(client, data); + } else if (type == CHUNK_THST) { + handleTHST(client, data); + } else if (type == CHUNK_THNM) { + handleTHNM(client, data); + } else if (type == CHUNK_STKL) { + handleSTKL(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a thread creation message. + * + * We should be tolerant of receiving a duplicate create message. (It + * shouldn't happen with the current implementation.) + */ + private void handleTHCR(Client client, ByteBuffer data) { + int threadId, nameLen; + String name; + + threadId = data.getInt(); + nameLen = data.getInt(); + name = getString(data, nameLen); + + Log.v("ddm-thread", "THCR: " + threadId + " '" + name + "'"); + + client.getClientData().addThread(threadId, name); + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a thread death message. + */ + private void handleTHDE(Client client, ByteBuffer data) { + int threadId; + + threadId = data.getInt(); + Log.v("ddm-thread", "THDE: " + threadId); + + client.getClientData().removeThread(threadId); + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a thread status update message. + * + * Response has: + * (1b) header len + * (1b) bytes per entry + * (2b) thread count + * Then, for each thread: + * (4b) threadId (matches value from THCR) + * (1b) thread status + * (4b) tid + * (4b) utime + * (4b) stime + */ + private void handleTHST(Client client, ByteBuffer data) { + int headerLen, bytesPerEntry, extraPerEntry; + int threadCount; + + headerLen = (data.get() & 0xff); + bytesPerEntry = (data.get() & 0xff); + threadCount = data.getShort(); + + headerLen -= 4; // we've read 4 bytes + while (headerLen-- > 0) + data.get(); + + extraPerEntry = bytesPerEntry - 18; // we want 18 bytes + + Log.v("ddm-thread", "THST: threadCount=" + threadCount); + + /* + * For each thread, extract the data, find the appropriate + * client, and add it to the ClientData. + */ + for (int i = 0; i < threadCount; i++) { + int threadId, status, tid, utime, stime; + boolean isDaemon = false; + + threadId = data.getInt(); + status = data.get(); + tid = data.getInt(); + utime = data.getInt(); + stime = data.getInt(); + if (bytesPerEntry >= 18) + isDaemon = (data.get() != 0); + + Log.v("ddm-thread", " id=" + threadId + + ", status=" + status + ", tid=" + tid + + ", utime=" + utime + ", stime=" + stime); + + ClientData cd = client.getClientData(); + ThreadInfo threadInfo = cd.getThread(threadId); + if (threadInfo != null) + threadInfo.updateThread(status, tid, utime, stime, isDaemon); + else + Log.d("ddms", "Thread with id=" + threadId + " not found"); + + // slurp up any extra + for (int slurp = extraPerEntry; slurp > 0; slurp--) + data.get(); + } + + client.update(Client.CHANGE_THREAD_DATA); + } + + /* + * Handle a THNM (THread NaMe) message. We get one of these after + * somebody calls Thread.setName() on a running thread. + */ + private void handleTHNM(Client client, ByteBuffer data) { + int threadId, nameLen; + String name; + + threadId = data.getInt(); + nameLen = data.getInt(); + name = getString(data, nameLen); + + Log.v("ddm-thread", "THNM: " + threadId + " '" + name + "'"); + + ThreadInfo threadInfo = client.getClientData().getThread(threadId); + if (threadInfo != null) { + threadInfo.setThreadName(name); + client.update(Client.CHANGE_THREAD_DATA); + } else { + Log.d("ddms", "Thread with id=" + threadId + " not found"); + } + } + + + /** + * Parse an incoming STKL. + */ + private void handleSTKL(Client client, ByteBuffer data) { + StackTraceElement[] trace; + int i, threadId, stackDepth; + @SuppressWarnings("unused") + int future; + + future = data.getInt(); + threadId = data.getInt(); + + Log.v("ddms", "STKL: " + threadId); + + /* un-serialize the StackTraceElement[] */ + stackDepth = data.getInt(); + trace = new StackTraceElement[stackDepth]; + for (i = 0; i < stackDepth; i++) { + String className, methodName, fileName; + int len, lineNumber; + + len = data.getInt(); + className = getString(data, len); + len = data.getInt(); + methodName = getString(data, len); + len = data.getInt(); + if (len == 0) { + fileName = null; + } else { + fileName = getString(data, len); + } + lineNumber = data.getInt(); + + trace[i] = new StackTraceElement(className, methodName, fileName, + lineNumber); + } + + ThreadInfo threadInfo = client.getClientData().getThread(threadId); + if (threadInfo != null) { + threadInfo.setStackCall(trace); + client.update(Client.CHANGE_THREAD_STACKTRACE); + } else { + Log.d("STKL", String.format( + "Got stackcall for thread %1$d, which does not exists (anymore?).", //$NON-NLS-1$ + threadId)); + } + } + + + /** + * Send a THEN (THread notification ENable) request to the client. + */ + public static void sendTHEN(Client client, boolean enable) + throws IOException { + + ByteBuffer rawBuf = allocBuffer(1); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + if (enable) + buf.put((byte)1); + else + buf.put((byte)0); + + finishChunkPacket(packet, CHUNK_THEN, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_THEN) + ": " + enable); + client.sendAndConsume(packet, mInst); + } + + + /** + * Send a STKL (STacK List) request to the client. The VM will suspend + * the target thread, obtain its stack, and return it. If the thread + * is no longer running, a failure result will be returned. + */ + public static void sendSTKL(Client client, int threadId) + throws IOException { + + if (false) { + Log.d("ddm-thread", "would send STKL " + threadId); + return; + } + + ByteBuffer rawBuf = allocBuffer(4); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + buf.putInt(threadId); + + finishChunkPacket(packet, CHUNK_STKL, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_STKL) + ": " + threadId); + client.sendAndConsume(packet, mInst); + } + + + /** + * This is called periodically from the UI thread. To avoid locking + * the UI while we request the updates, we create a new thread. + * + */ + static void requestThreadUpdate(final Client client) { + if (client.isDdmAware() && client.isThreadUpdateEnabled()) { + if (mThreadStatusReqRunning) { + Log.w("ddms", "Waiting for previous thread update req to finish"); + return; + } + + new Thread("Thread Status Req") { + @Override + public void run() { + mThreadStatusReqRunning = true; + try { + sendTHST(client); + } catch (IOException ioe) { + Log.d("ddms", "Unable to request thread updates from " + + client + ": " + ioe.getMessage()); + } finally { + mThreadStatusReqRunning = false; + } + } + }.start(); + } + } + + static void requestThreadStackCallRefresh(final Client client, final int threadId) { + if (client.isDdmAware() && client.isThreadUpdateEnabled()) { + if (mThreadStackTraceReqRunning ) { + Log.w("ddms", "Waiting for previous thread stack call req to finish"); + return; + } + + new Thread("Thread Status Req") { + @Override + public void run() { + mThreadStackTraceReqRunning = true; + try { + sendSTKL(client, threadId); + } catch (IOException ioe) { + Log.d("ddms", "Unable to request thread stack call updates from " + + client + ": " + ioe.getMessage()); + } finally { + mThreadStackTraceReqRunning = false; + } + } + }.start(); + } + + } + + /* + * Send a THST request to the specified client. + */ + private static void sendTHST(Client client) throws IOException { + ByteBuffer rawBuf = allocBuffer(0); + JdwpPacket packet = new JdwpPacket(rawBuf); + ByteBuffer buf = getChunkDataBuf(rawBuf); + + // nothing much to say + + finishChunkPacket(packet, CHUNK_THST, buf.position()); + Log.d("ddm-thread", "Sending " + name(CHUNK_THST)); + client.sendAndConsume(packet, mInst); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java new file mode 100644 index 000000000..934cbea1e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.ClientData.DebuggerStatus; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Handle the "wait" chunk (WAIT). These are sent up when the client is + * waiting for something, e.g. for a debugger to attach. + */ +final class HandleWait extends ChunkHandler { + + public static final int CHUNK_WAIT = ChunkHandler.type("WAIT"); + + private static final HandleWait mInst = new HandleWait(); + + + private HandleWait() {} + + /** + * Register for the packets we expect to get from the client. + */ + public static void register(MonitorThread mt) { + mt.registerChunkHandler(CHUNK_WAIT, mInst); + } + + /** + * Client is ready. + */ + @Override + public void clientReady(Client client) throws IOException {} + + /** + * Client went away. + */ + @Override + public void clientDisconnected(Client client) {} + + /** + * Chunk handler entry point. + */ + @Override + public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) { + + Log.d("ddm-wait", "handling " + ChunkHandler.name(type)); + + if (type == CHUNK_WAIT) { + assert !isReply; + handleWAIT(client, data); + } else { + handleUnknownChunk(client, type, data, isReply, msgId); + } + } + + /* + * Handle a reply to our WAIT message. + */ + private static void handleWAIT(Client client, ByteBuffer data) { + byte reason; + + reason = data.get(); + + Log.d("ddm-wait", "WAIT: reason=" + reason); + + + ClientData cd = client.getClientData(); + synchronized (cd) { + cd.setDebuggerConnectionStatus(DebuggerStatus.WAITING); + } + + client.update(Client.CHANGE_DEBUGGER_STATUS); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java new file mode 100644 index 000000000..6a62e6062 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.text.ParseException; + +/** + * Describes the types and locations of objects in a segment of a heap. + */ +public final class HeapSegment implements Comparable { + + /** + * Describes an object/region encoded in the HPSG data. + */ + public static class HeapSegmentElement implements Comparable { + + /* + * Solidity values, which must match the values in + * the HPSG data. + */ + + /** The element describes a free block. */ + public static int SOLIDITY_FREE = 0; + + /** The element is strongly-reachable. */ + public static int SOLIDITY_HARD = 1; + + /** The element is softly-reachable. */ + public static int SOLIDITY_SOFT = 2; + + /** The element is weakly-reachable. */ + public static int SOLIDITY_WEAK = 3; + + /** The element is phantom-reachable. */ + public static int SOLIDITY_PHANTOM = 4; + + /** The element is pending finalization. */ + public static int SOLIDITY_FINALIZABLE = 5; + + /** The element is not reachable, and is about to be swept/freed. */ + public static int SOLIDITY_SWEEP = 6; + + /** The reachability of the object is unknown. */ + public static int SOLIDITY_INVALID = -1; + + + /* + * Kind values, which must match the values in + * the HPSG data. + */ + + /** The element describes a data object. */ + public static int KIND_OBJECT = 0; + + /** The element describes a class object. */ + public static int KIND_CLASS_OBJECT = 1; + + /** The element describes an array of 1-byte elements. */ + public static int KIND_ARRAY_1 = 2; + + /** The element describes an array of 2-byte elements. */ + public static int KIND_ARRAY_2 = 3; + + /** The element describes an array of 4-byte elements. */ + public static int KIND_ARRAY_4 = 4; + + /** The element describes an array of 8-byte elements. */ + public static int KIND_ARRAY_8 = 5; + + /** The element describes an unknown type of object. */ + public static int KIND_UNKNOWN = 6; + + /** The element describes a native object. */ + public static int KIND_NATIVE = 7; + + /** The object kind is unknown or unspecified. */ + public static int KIND_INVALID = -1; + + + /** + * A bit in the HPSG data that indicates that an element should + * be combined with the element that follows, typically because + * an element is too large to be described by a single element. + */ + private static int PARTIAL_MASK = 1 << 7; + + + /** + * Describes the reachability/solidity of the element. Must + * be set to one of the SOLIDITY_* values. + */ + private int mSolidity; + + /** + * Describes the type/kind of the element. Must be set to one + * of the KIND_* values. + */ + private int mKind; + + /** + * Describes the length of the element, in bytes. + */ + private int mLength; + + + /** + * Creates an uninitialized element. + */ + public HeapSegmentElement() { + setSolidity(SOLIDITY_INVALID); + setKind(KIND_INVALID); + setLength(-1); + } + + /** + * Create an element describing the entry at the current + * position of hpsgData. + * + * @param hs The heap segment to pull the entry from. + * @throws BufferUnderflowException if there is not a whole entry + * following the current position + * of hpsgData. + * @throws ParseException if the provided data is malformed. + */ + public HeapSegmentElement(HeapSegment hs) + throws BufferUnderflowException, ParseException { + set(hs); + } + + /** + * Replace the element with the entry at the current position of + * hpsgData. + * + * @param hs The heap segment to pull the entry from. + * @return this object. + * @throws BufferUnderflowException if there is not a whole entry + * following the current position of + * hpsgData. + * @throws ParseException if the provided data is malformed. + */ + public HeapSegmentElement set(HeapSegment hs) + throws BufferUnderflowException, ParseException { + + /* TODO: Maybe keep track of the virtual address of each element + * so that they can be examined independently. + */ + ByteBuffer data = hs.mUsageData; + int eState = (int)data.get() & 0x000000ff; + int eLen = ((int)data.get() & 0x000000ff) + 1; + + while ((eState & PARTIAL_MASK) != 0) { + + /* If the partial bit was set, the next byte should describe + * the same object as the current one. + */ + int nextState = (int)data.get() & 0x000000ff; + if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) { + throw new ParseException("State mismatch", data.position()); + } + eState = nextState; + eLen += ((int)data.get() & 0x000000ff) + 1; + } + + setSolidity(eState & 0x7); + setKind((eState >> 3) & 0x7); + setLength(eLen * hs.mAllocationUnitSize); + + return this; + } + + public int getSolidity() { + return mSolidity; + } + + public void setSolidity(int solidity) { + this.mSolidity = solidity; + } + + public int getKind() { + return mKind; + } + + public void setKind(int kind) { + this.mKind = kind; + } + + public int getLength() { + return mLength; + } + + public void setLength(int length) { + this.mLength = length; + } + + public int compareTo(HeapSegmentElement other) { + if (mLength != other.mLength) { + return mLength < other.mLength ? -1 : 1; + } + return 0; + } + } + + //* The ID of the heap that this segment belongs to. + protected int mHeapId; + + //* The size of an allocation unit, in bytes. (e.g., 8 bytes) + protected int mAllocationUnitSize; + + //* The virtual address of the start of this segment. + protected long mStartAddress; + + //* The offset of this pices from mStartAddress, in bytes. + protected int mOffset; + + //* The number of allocation units described in this segment. + protected int mAllocationUnitCount; + + //* The raw data that describes the contents of this segment. + protected ByteBuffer mUsageData; + + //* mStartAddress is set to this value when the segment becomes invalid. + private final static long INVALID_START_ADDRESS = -1; + + /** + * Create a new HeapSegment based on the raw contents + * of an HPSG chunk. + * + * @param hpsgData The raw data from an HPSG chunk. + * @throws BufferUnderflowException if hpsgData is too small + * to hold the HPSG chunk header data. + */ + public HeapSegment(ByteBuffer hpsgData) throws BufferUnderflowException { + /* Read the HPSG chunk header. + * These get*() calls may throw a BufferUnderflowException + * if the underlying data isn't big enough. + */ + hpsgData.order(ByteOrder.BIG_ENDIAN); + mHeapId = hpsgData.getInt(); + mAllocationUnitSize = (int) hpsgData.get(); + mStartAddress = (long) hpsgData.getInt() & 0x00000000ffffffffL; + mOffset = hpsgData.getInt(); + mAllocationUnitCount = hpsgData.getInt(); + + // Hold onto the remainder of the data. + mUsageData = hpsgData.slice(); + mUsageData.order(ByteOrder.BIG_ENDIAN); // doesn't actually matter + + // Validate the data. +//xxx do it +//xxx make sure the number of elements matches mAllocationUnitCount. +//xxx make sure the last element doesn't have P set + } + + /** + * See if this segment still contains data, and has not been + * appended to another segment. + * + * @return true if this segment has not been appended to + * another segment. + */ + public boolean isValid() { + return mStartAddress != INVALID_START_ADDRESS; + } + + /** + * See if other comes immediately after this segment. + * + * @param other The HeapSegment to check. + * @return true if other comes immediately after this + * segment. + */ + public boolean canAppend(HeapSegment other) { + return isValid() && other.isValid() && mHeapId == other.mHeapId && + mAllocationUnitSize == other.mAllocationUnitSize && + getEndAddress() == other.getStartAddress(); + } + + /** + * Append the contents of other to this segment + * if it describes the segment immediately after this one. + * + * @param other The segment to append to this segment, if possible. + * If appended, other will be invalid + * when this method returns. + * @return true if other was successfully appended to + * this segment. + */ + public boolean append(HeapSegment other) { + if (canAppend(other)) { + /* Preserve the position. The mark is not preserved, + * but we don't use it anyway. + */ + int pos = mUsageData.position(); + + // Guarantee that we have enough room for the new data. + if (mUsageData.capacity() - mUsageData.limit() < + other.mUsageData.limit()) { + /* Grow more than necessary in case another append() + * is about to happen. + */ + int newSize = mUsageData.limit() + other.mUsageData.limit(); + ByteBuffer newData = ByteBuffer.allocate(newSize * 2); + + mUsageData.rewind(); + newData.put(mUsageData); + mUsageData = newData; + } + + // Copy the data from the other segment and restore the position. + other.mUsageData.rewind(); + mUsageData.put(other.mUsageData); + mUsageData.position(pos); + + // Fix this segment's header to cover the new data. + mAllocationUnitCount += other.mAllocationUnitCount; + + // Mark the other segment as invalid. + other.mStartAddress = INVALID_START_ADDRESS; + other.mUsageData = null; + + return true; + } else { + return false; + } + } + + public long getStartAddress() { + return mStartAddress + mOffset; + } + + public int getLength() { + return mAllocationUnitSize * mAllocationUnitCount; + } + + public long getEndAddress() { + return getStartAddress() + getLength(); + } + + public void rewindElements() { + if (mUsageData != null) { + mUsageData.rewind(); + } + } + + public HeapSegmentElement getNextElement(HeapSegmentElement reuse) { + try { + if (reuse != null) { + return reuse.set(this); + } else { + return new HeapSegmentElement(this); + } + } catch (BufferUnderflowException ex) { + /* Normal "end of buffer" situation. + */ + } catch (ParseException ex) { + /* Malformed data. + */ +//TODO: we should catch this in the constructor + } + return null; + } + + /* + * Method overrides for Comparable + */ + @Override + public boolean equals(Object o) { + if (o instanceof HeapSegment) { + return compareTo((HeapSegment) o) == 0; + } + return false; + } + + @Override + public int hashCode() { + return mHeapId * 31 + + mAllocationUnitSize * 31 + + (int) mStartAddress * 31 + + mOffset * 31 + + mAllocationUnitCount * 31 + + mUsageData.hashCode(); + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + + str.append("HeapSegment { heap ").append(mHeapId) + .append(", start 0x") + .append(Integer.toHexString((int) getStartAddress())) + .append(", length ").append(getLength()) + .append(" }"); + + return str.toString(); + } + + public int compareTo(HeapSegment other) { + if (mHeapId != other.mHeapId) { + return mHeapId < other.mHeapId ? -1 : 1; + } + if (getStartAddress() != other.getStartAddress()) { + return getStartAddress() < other.getStartAddress() ? -1 : 1; + } + + /* If two segments have the same start address, the rest of + * the fields should be equal. Go through the motions, though. + * Note that we re-check the components of getStartAddress() + * (mStartAddress and mOffset) to make sure that all fields in + * an equal segment are equal. + */ + + if (mAllocationUnitSize != other.mAllocationUnitSize) { + return mAllocationUnitSize < other.mAllocationUnitSize ? -1 : 1; + } + if (mStartAddress != other.mStartAddress) { + return mStartAddress < other.mStartAddress ? -1 : 1; + } + if (mOffset != other.mOffset) { + return mOffset < other.mOffset ? -1 : 1; + } + if (mAllocationUnitCount != other.mAllocationUnitCount) { + return mAllocationUnitCount < other.mAllocationUnitCount ? -1 : 1; + } + if (mUsageData != other.mUsageData) { + return mUsageData.compareTo(other.mUsageData); + } + return 0; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java new file mode 100755 index 000000000..8096abdb8 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java @@ -0,0 +1,264 @@ +/* + * 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.ddmlib; + +import com.android.ddmlib.log.LogReceiver; + +import java.io.IOException; +import java.util.Map; + + +/** + * A Device. It can be a physical device or an emulator. + */ +public interface IDevice { + + public final static String PROP_BUILD_VERSION = "ro.build.version.release"; + public final static String PROP_BUILD_API_LEVEL = "ro.build.version.sdk"; + public final static String PROP_BUILD_CODENAME = "ro.build.version.codename"; + + public final static String PROP_DEBUGGABLE = "ro.debuggable"; + + /** Serial number of the first connected emulator. */ + public final static String FIRST_EMULATOR_SN = "emulator-5554"; //$NON-NLS-1$ + /** Device change bit mask: {@link DeviceState} change. */ + public static final int CHANGE_STATE = 0x0001; + /** Device change bit mask: {@link Client} list change. */ + public static final int CHANGE_CLIENT_LIST = 0x0002; + /** Device change bit mask: build info change. */ + public static final int CHANGE_BUILD_INFO = 0x0004; + + /** @deprecated Use {@link #PROP_BUILD_API_LEVEL}. */ + public final static String PROP_BUILD_VERSION_NUMBER = PROP_BUILD_API_LEVEL; + + /** + * The state of a device. + */ + public static enum DeviceState { + BOOTLOADER("bootloader"), //$NON-NLS-1$ + OFFLINE("offline"), //$NON-NLS-1$ + ONLINE("device"); //$NON-NLS-1$ + + private String mState; + + DeviceState(String state) { + mState = state; + } + + /** + * Returns a {@link DeviceState} from the string returned by adb devices. + * @param state the device state. + * @return a {@link DeviceState} object or null if the state is unknown. + */ + public static DeviceState getState(String state) { + for (DeviceState deviceState : values()) { + if (deviceState.mState.equals(state)) { + return deviceState; + } + } + return null; + } + } + + /** + * Returns the serial number of the device. + */ + public String getSerialNumber(); + + /** + * Returns the name of the AVD the emulator is running. + *

This is only valid if {@link #isEmulator()} returns true. + *

If the emulator is not running any AVD (for instance it's running from an Android source + * tree build), this method will return "<build>". + * @return the name of the AVD or null if there isn't any. + */ + public String getAvdName(); + + /** + * Returns the state of the device. + */ + public DeviceState getState(); + + /** + * Returns the device properties. It contains the whole output of 'getprop' + */ + public Map getProperties(); + + /** + * Returns the number of property for this device. + */ + public int getPropertyCount(); + + /** + * Returns a property value. + * @param name the name of the value to return. + * @return the value or null if the property does not exist. + */ + public String getProperty(String name); + + /** + * Returns if the device is ready. + * @return true if {@link #getState()} returns {@link DeviceState#ONLINE}. + */ + public boolean isOnline(); + + /** + * Returns true if the device is an emulator. + */ + public boolean isEmulator(); + + /** + * Returns if the device is offline. + * @return true if {@link #getState()} returns {@link DeviceState#OFFLINE}. + */ + public boolean isOffline(); + + /** + * Returns if the device is in bootloader mode. + * @return true if {@link #getState()} returns {@link DeviceState#BOOTLOADER}. + */ + public boolean isBootLoader(); + + /** + * Returns whether the {@link Device} has {@link Client}s. + */ + public boolean hasClients(); + + /** + * Returns the array of clients. + */ + public Client[] getClients(); + + /** + * Returns a {@link Client} by its application name. + * @param applicationName the name of the application + * @return the Client object or null if no match was found. + */ + public Client getClient(String applicationName); + + /** + * Returns a {@link SyncService} object to push / pull files to and from the device. + * @return null if the SyncService couldn't be created. This can happen if adb + * refuse to open the connection because the {@link IDevice} is invalid (or got disconnected). + * @throws IOException if the connection with adb failed. + */ + public SyncService getSyncService() throws IOException; + + /** + * Returns a {@link FileListingService} for this device. + */ + public FileListingService getFileListingService(); + + /** + * Takes a screen shot of the device and returns it as a {@link RawImage}. + * @return the screenshot as a RawImage or null if + * something went wrong. + * @throws IOException + */ + public RawImage getScreenshot() throws IOException; + + /** + * Executes a shell command on the device, and sends the result to a receiver. + * @param command The command to execute + * @param receiver The receiver object getting the result from the command. + * @throws IOException + */ + public void executeShellCommand(String command, + IShellOutputReceiver receiver) throws IOException; + + /** + * Runs the event log service and outputs the event log to the {@link LogReceiver}. + * @param receiver the receiver to receive the event log entries. + * @throws IOException + */ + public void runEventLogService(LogReceiver receiver) throws IOException; + + /** + * Runs the log service for the given log and outputs the log to the {@link LogReceiver}. + * @param logname the logname of the log to read from. + * @param receiver the receiver to receive the event log entries. + * @throws IOException + */ + public void runLogService(String logname, LogReceiver receiver) throws IOException; + + /** + * Creates a port forwarding between a local and a remote port. + * @param localPort the local port to forward + * @param remotePort the remote port. + * @return true if success. + */ + public boolean createForward(int localPort, int remotePort); + + /** + * Removes a port forwarding between a local and a remote port. + * @param localPort the local port to forward + * @param remotePort the remote port. + * @return true if success. + */ + public boolean removeForward(int localPort, int remotePort); + + /** + * Returns the name of the client by pid or null if pid is unknown + * @param pid the pid of the client. + */ + public String getClientName(int pid); + + /** + * Installs an Android application on device. + * This is a helper method that combines the syncPackageToDevice, installRemotePackage, + * and removePackage steps + * @param packageFilePath the absolute file system path to file on local host to install + * @param reinstall set to true if re-install of app should be performed + * @return a {@link String} with an error code, or null if success. + * @throws IOException + */ + public String installPackage(String packageFilePath, boolean reinstall) throws IOException; + + /** + * Pushes a file to device + * @param localFilePath the absolute path to file on local host + * @return {@link String} destination path on device for file + * @throws IOException if fatal error occurred when pushing file + */ + public String syncPackageToDevice(String localFilePath) + throws IOException; + + /** + * Installs the application package that was pushed to a temporary location on the device. + * @param remoteFilePath absolute file path to package file on device + * @param reinstall set to true if re-install of app should be performed + * @throws InstallException if installation failed + */ + public String installRemotePackage(String remoteFilePath, boolean reinstall) + throws IOException; + + /** + * Remove a file from device + * @param remoteFilePath path on device of file to remove + * @throws IOException if file removal failed + */ + public void removeRemotePackage(String remoteFilePath) throws IOException; + + /** + * Uninstall an package from the device. + * @param packageName the Android application package name to uninstall + * @return a {@link String} with an error code, or null if success. + * @throws IOException + */ + public String uninstallPackage(String packageName) throws IOException; + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java new file mode 100644 index 000000000..fb671bb86 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +/** + * Classes which implement this interface provide methods that deal with out from a remote shell + * command on a device/emulator. + */ +public interface IShellOutputReceiver { + /** + * Called every time some new data is available. + * @param data The new data. + * @param offset The offset at which the new data starts. + * @param length The length of the new data. + */ + public void addOutput(byte[] data, int offset, int length); + + /** + * Called at the end of the process execution (unless the process was + * canceled). This allows the receiver to terminate and flush whatever + * data was not yet processed. + */ + public void flush(); + + /** + * Cancel method to stop the execution of the remote shell command. + * @return true to cancel the execution of the command. + */ + public boolean isCancelled(); +}; diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java new file mode 100644 index 000000000..3b9d730c4 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +/** + * Classes which implement this interface provide a method that returns a stack trace. + */ +public interface IStackTraceInfo { + + /** + * Returns the stack trace. This can be null. + */ + public StackTraceElement[] getStackTrace(); + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java new file mode 100644 index 000000000..92bbb8270 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java @@ -0,0 +1,371 @@ +/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java +** +** Copyright 2007, 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.ddmlib; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; + +/** + * A JDWP packet, sitting at the start of a ByteBuffer somewhere. + * + * This allows us to wrap a "pointer" to the data with the results of + * decoding the packet. + * + * None of the operations here are synchronized. If multiple threads will + * be accessing the same ByteBuffers, external sync will be required. + * + * Use the constructor to create an empty packet, or "findPacket()" to + * wrap a JdwpPacket around existing data. + */ +final class JdwpPacket { + // header len + public static final int JDWP_HEADER_LEN = 11; + + // results from findHandshake + public static final int HANDSHAKE_GOOD = 1; + public static final int HANDSHAKE_NOTYET = 2; + public static final int HANDSHAKE_BAD = 3; + + // our cmdSet/cmd + private static final int DDMS_CMD_SET = 0xc7; // 'G' + 128 + private static final int DDMS_CMD = 0x01; + + // "flags" field + private static final int REPLY_PACKET = 0x80; + + // this is sent and expected at the start of a JDWP connection + private static final byte[] mHandshake = { + 'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e' + }; + + public static final int HANDSHAKE_LEN = mHandshake.length; + + private ByteBuffer mBuffer; + private int mLength, mId, mFlags, mCmdSet, mCmd, mErrCode; + private boolean mIsNew; + + private static int mSerialId = 0x40000000; + + + /** + * Create a new, empty packet, in "buf". + */ + JdwpPacket(ByteBuffer buf) { + mBuffer = buf; + mIsNew = true; + } + + /** + * Finish a packet created with newPacket(). + * + * This always creates a command packet, with the next serial number + * in sequence. + * + * We have to take "payloadLength" as an argument because we can't + * see the position in the "slice" returned by getPayload(). We could + * fish it out of the chunk header, but it's legal for there to be + * more than one chunk in a JDWP packet. + * + * On exit, "position" points to the end of the data. + */ + void finishPacket(int payloadLength) { + assert mIsNew; + + ByteOrder oldOrder = mBuffer.order(); + mBuffer.order(ChunkHandler.CHUNK_ORDER); + + mLength = JDWP_HEADER_LEN + payloadLength; + mId = getNextSerial(); + mFlags = 0; + mCmdSet = DDMS_CMD_SET; + mCmd = DDMS_CMD; + + mBuffer.putInt(0x00, mLength); + mBuffer.putInt(0x04, mId); + mBuffer.put(0x08, (byte) mFlags); + mBuffer.put(0x09, (byte) mCmdSet); + mBuffer.put(0x0a, (byte) mCmd); + + mBuffer.order(oldOrder); + mBuffer.position(mLength); + } + + /** + * Get the next serial number. This creates a unique serial number + * across all connections, not just for the current connection. This + * is a useful property when debugging, but isn't necessary. + * + * We can't synchronize on an int, so we use a sync method. + */ + private static synchronized int getNextSerial() { + return mSerialId++; + } + + /** + * Return a slice of the byte buffer, positioned past the JDWP header + * to the start of the chunk header. The buffer's limit will be set + * to the size of the payload if the size is known; if this is a + * packet under construction the limit will be set to the end of the + * buffer. + * + * Doesn't examine the packet at all -- works on empty buffers. + */ + ByteBuffer getPayload() { + ByteBuffer buf; + int oldPosn = mBuffer.position(); + + mBuffer.position(JDWP_HEADER_LEN); + buf = mBuffer.slice(); // goes from position to limit + mBuffer.position(oldPosn); + + if (mLength > 0) + buf.limit(mLength - JDWP_HEADER_LEN); + else + assert mIsNew; + buf.order(ChunkHandler.CHUNK_ORDER); + return buf; + } + + /** + * Returns "true" if this JDWP packet has a JDWP command type. + * + * This never returns "true" for reply packets. + */ + boolean isDdmPacket() { + return (mFlags & REPLY_PACKET) == 0 && + mCmdSet == DDMS_CMD_SET && + mCmd == DDMS_CMD; + } + + /** + * Returns "true" if this JDWP packet is tagged as a reply. + */ + boolean isReply() { + return (mFlags & REPLY_PACKET) != 0; + } + + /** + * Returns "true" if this JDWP packet is a reply with a nonzero + * error code. + */ + boolean isError() { + return isReply() && mErrCode != 0; + } + + /** + * Returns "true" if this JDWP packet has no data. + */ + boolean isEmpty() { + return (mLength == JDWP_HEADER_LEN); + } + + /** + * Return the packet's ID. For a reply packet, this allows us to + * match the reply with the original request. + */ + int getId() { + return mId; + } + + /** + * Return the length of a packet. This includes the header, so an + * empty packet is 11 bytes long. + */ + int getLength() { + return mLength; + } + + /** + * Write our packet to "chan". Consumes the packet as part of the + * write. + * + * The JDWP packet starts at offset 0 and ends at mBuffer.position(). + */ + void writeAndConsume(SocketChannel chan) throws IOException { + int oldLimit; + + //Log.i("ddms", "writeAndConsume: pos=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + + assert mLength > 0; + + mBuffer.flip(); // limit<-posn, posn<-0 + oldLimit = mBuffer.limit(); + mBuffer.limit(mLength); + while (mBuffer.position() != mBuffer.limit()) { + chan.write(mBuffer); + } + // position should now be at end of packet + assert mBuffer.position() == mLength; + + mBuffer.limit(oldLimit); + mBuffer.compact(); // shift posn...limit, posn<-pending data + + //Log.i("ddms", " : pos=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + } + + /** + * "Move" the packet data out of the buffer we're sitting on and into + * buf at the current position. + */ + void movePacket(ByteBuffer buf) { + Log.v("ddms", "moving " + mLength + " bytes"); + int oldPosn = mBuffer.position(); + + mBuffer.position(0); + mBuffer.limit(mLength); + buf.put(mBuffer); + mBuffer.position(mLength); + mBuffer.limit(oldPosn); + mBuffer.compact(); // shift posn...limit, posn<-pending data + } + + /** + * Consume the JDWP packet. + * + * On entry and exit, "position" is the #of bytes in the buffer. + */ + void consume() + { + //Log.d("ddms", "consuming " + mLength + " bytes"); + //Log.d("ddms", " posn=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + + /* + * The "flip" call sets "limit" equal to the position (usually the + * end of data) and "position" equal to zero. + * + * compact() copies everything from "position" and "limit" to the + * start of the buffer, sets "position" to the end of data, and + * sets "limit" to the capacity. + * + * On entry, "position" is set to the amount of data in the buffer + * and "limit" is set to the capacity. We want to call flip() + * so that position..limit spans our data, advance "position" past + * the current packet, then compact. + */ + mBuffer.flip(); // limit<-posn, posn<-0 + mBuffer.position(mLength); + mBuffer.compact(); // shift posn...limit, posn<-pending data + mLength = 0; + //Log.d("ddms", " after compact, posn=" + mBuffer.position() + // + ", limit=" + mBuffer.limit()); + } + + /** + * Find the JDWP packet at the start of "buf". The start is known, + * but the length has to be parsed out. + * + * On entry, the packet data in "buf" must start at offset 0 and end + * at "position". "limit" should be set to the buffer capacity. This + * method does not alter "buf"s attributes. + * + * Returns a new JdwpPacket if a full one is found in the buffer. If + * not, returns null. Throws an exception if the data doesn't look like + * a valid JDWP packet. + */ + static JdwpPacket findPacket(ByteBuffer buf) { + int count = buf.position(); + int length, id, flags, cmdSet, cmd; + + if (count < JDWP_HEADER_LEN) + return null; + + ByteOrder oldOrder = buf.order(); + buf.order(ChunkHandler.CHUNK_ORDER); + + length = buf.getInt(0x00); + id = buf.getInt(0x04); + flags = buf.get(0x08) & 0xff; + cmdSet = buf.get(0x09) & 0xff; + cmd = buf.get(0x0a) & 0xff; + + buf.order(oldOrder); + + if (length < JDWP_HEADER_LEN) + throw new BadPacketException(); + if (count < length) + return null; + + JdwpPacket pkt = new JdwpPacket(buf); + //pkt.mBuffer = buf; + pkt.mLength = length; + pkt.mId = id; + pkt.mFlags = flags; + + if ((flags & REPLY_PACKET) == 0) { + pkt.mCmdSet = cmdSet; + pkt.mCmd = cmd; + pkt.mErrCode = -1; + } else { + pkt.mCmdSet = -1; + pkt.mCmd = -1; + pkt.mErrCode = cmdSet | (cmd << 8); + } + + return pkt; + } + + /** + * Like findPacket(), but when we're expecting the JDWP handshake. + * + * Returns one of: + * HANDSHAKE_GOOD - found handshake, looks good + * HANDSHAKE_BAD - found enough data, but it's wrong + * HANDSHAKE_NOTYET - not enough data has been read yet + */ + static int findHandshake(ByteBuffer buf) { + int count = buf.position(); + int i; + + if (count < mHandshake.length) + return HANDSHAKE_NOTYET; + + for (i = mHandshake.length -1; i >= 0; --i) { + if (buf.get(i) != mHandshake[i]) + return HANDSHAKE_BAD; + } + + return HANDSHAKE_GOOD; + } + + /** + * Remove the handshake string from the buffer. + * + * On entry and exit, "position" is the #of bytes in the buffer. + */ + static void consumeHandshake(ByteBuffer buf) { + // in theory, nothing else can have arrived, so this is overkill + buf.flip(); // limit<-posn, posn<-0 + buf.position(mHandshake.length); + buf.compact(); // shift posn...limit, posn<-pending data + } + + /** + * Copy the handshake string into the output buffer. + * + * On exit, "buf"s position will be advanced. + */ + static void putHandshake(ByteBuffer buf) { + buf.put(mHandshake); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java new file mode 100644 index 000000000..ce95b0454 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Log class that mirrors the API in main Android sources. + *

Default behavior outputs the log to {@link System#out}. Use + * {@link #setLogOutput(com.android.ddmlib.Log.ILogOutput)} to redirect the log somewhere else. + */ +public final class Log { + + /** + * Log Level enum. + */ + public enum LogLevel { + VERBOSE(2, "verbose", 'V'), //$NON-NLS-1$ + DEBUG(3, "debug", 'D'), //$NON-NLS-1$ + INFO(4, "info", 'I'), //$NON-NLS-1$ + WARN(5, "warn", 'W'), //$NON-NLS-1$ + ERROR(6, "error", 'E'), //$NON-NLS-1$ + ASSERT(7, "assert", 'A'); //$NON-NLS-1$ + + private int mPriorityLevel; + private String mStringValue; + private char mPriorityLetter; + + LogLevel(int intPriority, String stringValue, char priorityChar) { + mPriorityLevel = intPriority; + mStringValue = stringValue; + mPriorityLetter = priorityChar; + } + + public static LogLevel getByString(String value) { + for (LogLevel mode : values()) { + if (mode.mStringValue.equals(value)) { + return mode; + } + } + + return null; + } + + /** + * Returns the {@link LogLevel} enum matching the specified letter. + * @param letter the letter matching a LogLevel enum + * @return a LogLevel object or null if no match were found. + */ + public static LogLevel getByLetter(char letter) { + for (LogLevel mode : values()) { + if (mode.mPriorityLetter == letter) { + return mode; + } + } + + return null; + } + + /** + * Returns the {@link LogLevel} enum matching the specified letter. + *

+ * The letter is passed as a {@link String} argument, but only the first character + * is used. + * @param letter the letter matching a LogLevel enum + * @return a LogLevel object or null if no match were found. + */ + public static LogLevel getByLetterString(String letter) { + if (letter.length() > 0) { + return getByLetter(letter.charAt(0)); + } + + return null; + } + + /** + * Returns the letter identifying the priority of the {@link LogLevel}. + */ + public char getPriorityLetter() { + return mPriorityLetter; + } + + /** + * Returns the numerical value of the priority. + */ + public int getPriority() { + return mPriorityLevel; + } + + /** + * Returns a non translated string representing the LogLevel. + */ + public String getStringValue() { + return mStringValue; + } + } + + /** + * Classes which implement this interface provides methods that deal with outputting log + * messages. + */ + public interface ILogOutput { + /** + * Sent when a log message needs to be printed. + * @param logLevel The {@link LogLevel} enum representing the priority of the message. + * @param tag The tag associated with the message. + * @param message The message to display. + */ + public void printLog(LogLevel logLevel, String tag, String message); + + /** + * Sent when a log message needs to be printed, and, if possible, displayed to the user + * in a dialog box. + * @param logLevel The {@link LogLevel} enum representing the priority of the message. + * @param tag The tag associated with the message. + * @param message The message to display. + */ + public void printAndPromptLog(LogLevel logLevel, String tag, String message); + } + + private static LogLevel mLevel = DdmPreferences.getLogLevel(); + + private static ILogOutput sLogOutput; + + private static final char[] mSpaceLine = new char[72]; + private static final char[] mHexDigit = new char[] + { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; + static { + /* prep for hex dump */ + int i = mSpaceLine.length-1; + while (i >= 0) + mSpaceLine[i--] = ' '; + mSpaceLine[0] = mSpaceLine[1] = mSpaceLine[2] = mSpaceLine[3] = '0'; + mSpaceLine[4] = '-'; + } + + static final class Config { + static final boolean LOGV = true; + static final boolean LOGD = true; + }; + + private Log() {} + + /** + * Outputs a {@link LogLevel#VERBOSE} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void v(String tag, String message) { + println(LogLevel.VERBOSE, tag, message); + } + + /** + * Outputs a {@link LogLevel#DEBUG} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void d(String tag, String message) { + println(LogLevel.DEBUG, tag, message); + } + + /** + * Outputs a {@link LogLevel#INFO} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void i(String tag, String message) { + println(LogLevel.INFO, tag, message); + } + + /** + * Outputs a {@link LogLevel#WARN} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void w(String tag, String message) { + println(LogLevel.WARN, tag, message); + } + + /** + * Outputs a {@link LogLevel#ERROR} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void e(String tag, String message) { + println(LogLevel.ERROR, tag, message); + } + + /** + * Outputs a log message and attempts to display it in a dialog. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void logAndDisplay(LogLevel logLevel, String tag, String message) { + if (sLogOutput != null) { + sLogOutput.printAndPromptLog(logLevel, tag, message); + } else { + println(logLevel, tag, message); + } + } + + /** + * Outputs a {@link LogLevel#ERROR} level {@link Throwable} information. + * @param tag The tag associated with the message. + * @param throwable The {@link Throwable} to output. + */ + public static void e(String tag, Throwable throwable) { + if (throwable != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + throwable.printStackTrace(pw); + println(LogLevel.ERROR, tag, throwable.getMessage() + '\n' + sw.toString()); + } + } + + static void setLevel(LogLevel logLevel) { + mLevel = logLevel; + } + + /** + * Sets the {@link ILogOutput} to use to print the logs. If not set, {@link System#out} + * will be used. + * @param logOutput The {@link ILogOutput} to use to print the log. + */ + public static void setLogOutput(ILogOutput logOutput) { + sLogOutput = logOutput; + } + + /** + * Show hex dump. + *

+ * Local addition. Output looks like: + * 1230- 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 0123456789abcdef + *

+ * Uses no string concatenation; creates one String object per line. + */ + static void hexDump(String tag, LogLevel level, byte[] data, int offset, int length) { + + int kHexOffset = 6; + int kAscOffset = 55; + char[] line = new char[mSpaceLine.length]; + int addr, baseAddr, count; + int i, ch; + boolean needErase = true; + + //Log.w(tag, "HEX DUMP: off=" + offset + ", length=" + length); + + baseAddr = 0; + while (length != 0) { + if (length > 16) { + // full line + count = 16; + } else { + // partial line; re-copy blanks to clear end + count = length; + needErase = true; + } + + if (needErase) { + System.arraycopy(mSpaceLine, 0, line, 0, mSpaceLine.length); + needErase = false; + } + + // output the address (currently limited to 4 hex digits) + addr = baseAddr; + addr &= 0xffff; + ch = 3; + while (addr != 0) { + line[ch] = mHexDigit[addr & 0x0f]; + ch--; + addr >>>= 4; + } + + // output hex digits and ASCII chars + ch = kHexOffset; + for (i = 0; i < count; i++) { + byte val = data[offset + i]; + + line[ch++] = mHexDigit[(val >>> 4) & 0x0f]; + line[ch++] = mHexDigit[val & 0x0f]; + ch++; + + if (val >= 0x20 && val < 0x7f) + line[kAscOffset + i] = (char) val; + else + line[kAscOffset + i] = '.'; + } + + println(level, tag, new String(line)); + + // advance to next chunk of data + length -= count; + offset += count; + baseAddr += count; + } + + } + + /** + * Dump the entire contents of a byte array with DEBUG priority. + */ + static void hexDump(byte[] data) { + hexDump("ddms", LogLevel.DEBUG, data, 0, data.length); + } + + /* currently prints to stdout; could write to a log window */ + private static void println(LogLevel logLevel, String tag, String message) { + if (logLevel.getPriority() >= mLevel.getPriority()) { + if (sLogOutput != null) { + sLogOutput.printLog(logLevel, tag, message); + } else { + printLog(logLevel, tag, message); + } + } + } + + /** + * Prints a log message. + * @param logLevel + * @param tag + * @param message + */ + public static void printLog(LogLevel logLevel, String tag, String message) { + long msec; + + msec = System.currentTimeMillis(); + String outMessage = String.format("%02d:%02d %c/%s: %s\n", + (msec / 60000) % 60, (msec / 1000) % 60, + logLevel.getPriorityLetter(), tag, message); + System.out.print(outMessage); + } + +} + + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java new file mode 100644 index 000000000..a51494541 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java @@ -0,0 +1,780 @@ +/* + * Copyright (C) 2007 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.ddmlib; + + +import com.android.ddmlib.DebugPortManager.IDebugPortProvider; +import com.android.ddmlib.Log.LogLevel; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.NotYetBoundException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Monitor open connections. + */ +final class MonitorThread extends Thread { + + // For broadcasts to message handlers + //private static final int CLIENT_CONNECTED = 1; + + private static final int CLIENT_READY = 2; + + private static final int CLIENT_DISCONNECTED = 3; + + private volatile boolean mQuit = false; + + // List of clients we're paying attention to + private ArrayList mClientList; + + // The almighty mux + private Selector mSelector; + + // Map chunk types to handlers + private HashMap mHandlerMap; + + // port for "debug selected" + private ServerSocketChannel mDebugSelectedChan; + + private int mNewDebugSelectedPort; + + private int mDebugSelectedPort = -1; + + /** + * "Selected" client setup to answer debugging connection to the mNewDebugSelectedPort port. + */ + private Client mSelectedClient = null; + + // singleton + private static MonitorThread mInstance; + + /** + * Generic constructor. + */ + private MonitorThread() { + super("Monitor"); + mClientList = new ArrayList(); + mHandlerMap = new HashMap(); + + mNewDebugSelectedPort = DdmPreferences.getSelectedDebugPort(); + } + + /** + * Creates and return the singleton instance of the client monitor thread. + */ + static MonitorThread createInstance() { + return mInstance = new MonitorThread(); + } + + /** + * Get singleton instance of the client monitor thread. + */ + static MonitorThread getInstance() { + return mInstance; + } + + + /** + * Sets or changes the port number for "debug selected". + */ + synchronized void setDebugSelectedPort(int port) throws IllegalStateException { + if (mInstance == null) { + return; + } + + if (AndroidDebugBridge.getClientSupport() == false) { + return; + } + + if (mDebugSelectedChan != null) { + Log.d("ddms", "Changing debug-selected port to " + port); + mNewDebugSelectedPort = port; + wakeup(); + } else { + // we set mNewDebugSelectedPort instead of mDebugSelectedPort so that it's automatically + // opened on the first run loop. + mNewDebugSelectedPort = port; + } + } + + /** + * Sets the client to accept debugger connection on the custom "Selected debug port". + * @param selectedClient the client. Can be null. + */ + synchronized void setSelectedClient(Client selectedClient) { + if (mInstance == null) { + return; + } + + if (mSelectedClient != selectedClient) { + Client oldClient = mSelectedClient; + mSelectedClient = selectedClient; + + if (oldClient != null) { + oldClient.update(Client.CHANGE_PORT); + } + + if (mSelectedClient != null) { + mSelectedClient.update(Client.CHANGE_PORT); + } + } + } + + /** + * Returns the client accepting debugger connection on the custom "Selected debug port". + */ + Client getSelectedClient() { + return mSelectedClient; + } + + + /** + * Returns "true" if we want to retry connections to clients if we get a bad + * JDWP handshake back, "false" if we want to just mark them as bad and + * leave them alone. + */ + boolean getRetryOnBadHandshake() { + return true; // TODO? make configurable + } + + /** + * Get an array of known clients. + */ + Client[] getClients() { + synchronized (mClientList) { + return mClientList.toArray(new Client[0]); + } + } + + /** + * Register "handler" as the handler for type "type". + */ + synchronized void registerChunkHandler(int type, ChunkHandler handler) { + if (mInstance == null) { + return; + } + + synchronized (mHandlerMap) { + if (mHandlerMap.get(type) == null) { + mHandlerMap.put(type, handler); + } + } + } + + /** + * Watch for activity from clients and debuggers. + */ + @Override + public void run() { + Log.d("ddms", "Monitor is up"); + + // create a selector + try { + mSelector = Selector.open(); + } catch (IOException ioe) { + Log.logAndDisplay(LogLevel.ERROR, "ddms", + "Failed to initialize Monitor Thread: " + ioe.getMessage()); + return; + } + + while (!mQuit) { + + try { + /* + * sync with new registrations: we wait until addClient is done before going through + * and doing mSelector.select() again. + * @see {@link #addClient(Client)} + */ + synchronized (mClientList) { + } + + // (re-)open the "debug selected" port, if it's not opened yet or + // if the port changed. + try { + if (AndroidDebugBridge.getClientSupport()) { + if ((mDebugSelectedChan == null || + mNewDebugSelectedPort != mDebugSelectedPort) && + mNewDebugSelectedPort != -1) { + if (reopenDebugSelectedPort()) { + mDebugSelectedPort = mNewDebugSelectedPort; + } + } + } + } catch (IOException ioe) { + Log.e("ddms", + "Failed to reopen debug port for Selected Client to: " + mNewDebugSelectedPort); + Log.e("ddms", ioe); + mNewDebugSelectedPort = mDebugSelectedPort; // no retry + } + + int count; + try { + count = mSelector.select(); + } catch (IOException ioe) { + ioe.printStackTrace(); + continue; + } catch (CancelledKeyException cke) { + continue; + } + + if (count == 0) { + // somebody called wakeup() ? + // Log.i("ddms", "selector looping"); + continue; + } + + Set keys = mSelector.selectedKeys(); + Iterator iter = keys.iterator(); + + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + + try { + if (key.attachment() instanceof Client) { + processClientActivity(key); + } + else if (key.attachment() instanceof Debugger) { + processDebuggerActivity(key); + } + else if (key.attachment() instanceof MonitorThread) { + processDebugSelectedActivity(key); + } + else { + Log.e("ddms", "unknown activity key"); + } + } catch (Exception e) { + // we don't want to have our thread be killed because of any uncaught + // exception, so we intercept all here. + Log.e("ddms", "Exception during activity from Selector."); + Log.e("ddms", e); + } + } + } catch (Exception e) { + // we don't want to have our thread be killed because of any uncaught + // exception, so we intercept all here. + Log.e("ddms", "Exception MonitorThread.run()"); + Log.e("ddms", e); + } + } + } + + + /** + * Returns the port on which the selected client listen for debugger + */ + int getDebugSelectedPort() { + return mDebugSelectedPort; + } + + /* + * Something happened. Figure out what. + */ + private void processClientActivity(SelectionKey key) { + Client client = (Client)key.attachment(); + + try { + if (key.isReadable() == false || key.isValid() == false) { + Log.d("ddms", "Invalid key from " + client + ". Dropping client."); + dropClient(client, true /* notify */); + return; + } + + client.read(); + + /* + * See if we have a full packet in the buffer. It's possible we have + * more than one packet, so we have to loop. + */ + JdwpPacket packet = client.getJdwpPacket(); + while (packet != null) { + if (packet.isDdmPacket()) { + // unsolicited DDM request - hand it off + assert !packet.isReply(); + callHandler(client, packet, null); + packet.consume(); + } else if (packet.isReply() + && client.isResponseToUs(packet.getId()) != null) { + // reply to earlier DDM request + ChunkHandler handler = client + .isResponseToUs(packet.getId()); + if (packet.isError()) + client.packetFailed(packet); + else if (packet.isEmpty()) + Log.d("ddms", "Got empty reply for 0x" + + Integer.toHexString(packet.getId()) + + " from " + client); + else + callHandler(client, packet, handler); + packet.consume(); + client.removeRequestId(packet.getId()); + } else { + Log.v("ddms", "Forwarding client " + + (packet.isReply() ? "reply" : "event") + " 0x" + + Integer.toHexString(packet.getId()) + " to " + + client.getDebugger()); + client.forwardPacketToDebugger(packet); + } + + // find next + packet = client.getJdwpPacket(); + } + } catch (CancelledKeyException e) { + // key was canceled probably due to a disconnected client before we could + // read stuff coming from the client, so we drop it. + dropClient(client, true /* notify */); + } catch (IOException ex) { + // something closed down, no need to print anything. The client is simply dropped. + dropClient(client, true /* notify */); + } catch (Exception ex) { + Log.e("ddms", ex); + + /* close the client; automatically un-registers from selector */ + dropClient(client, true /* notify */); + + if (ex instanceof BufferOverflowException) { + Log.w("ddms", + "Client data packet exceeded maximum buffer size " + + client); + } else { + // don't know what this is, display it + Log.e("ddms", ex); + } + } + } + + /* + * Process an incoming DDM packet. If this is a reply to an earlier request, + * "handler" will be set to the handler responsible for the original + * request. The spec allows a JDWP message to include multiple DDM chunks. + */ + private void callHandler(Client client, JdwpPacket packet, + ChunkHandler handler) { + + // on first DDM packet received, broadcast a "ready" message + if (!client.ddmSeen()) + broadcast(CLIENT_READY, client); + + ByteBuffer buf = packet.getPayload(); + int type, length; + boolean reply = true; + + type = buf.getInt(); + length = buf.getInt(); + + if (handler == null) { + // not a reply, figure out who wants it + synchronized (mHandlerMap) { + handler = mHandlerMap.get(type); + reply = false; + } + } + + if (handler == null) { + Log.w("ddms", "Received unsupported chunk type " + + ChunkHandler.name(type) + " (len=" + length + ")"); + } else { + Log.d("ddms", "Calling handler for " + ChunkHandler.name(type) + + " [" + handler + "] (len=" + length + ")"); + ByteBuffer ibuf = buf.slice(); + ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O + roBuf.order(ChunkHandler.CHUNK_ORDER); + // do the handling of the chunk synchronized on the client list + // to be sure there's no concurrency issue when we look for HOME + // in hasApp() + synchronized (mClientList) { + handler.handleChunk(client, type, roBuf, reply, packet.getId()); + } + } + } + + /** + * Drops a client from the monitor. + *

This will lock the {@link Client} list of the {@link Device} running client. + * @param client + * @param notify + */ + synchronized void dropClient(Client client, boolean notify) { + if (mInstance == null) { + return; + } + + synchronized (mClientList) { + if (mClientList.remove(client) == false) { + return; + } + } + client.close(notify); + broadcast(CLIENT_DISCONNECTED, client); + + /* + * http://forum.java.sun.com/thread.jspa?threadID=726715&start=0 + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5073504 + */ + wakeup(); + } + + /* + * Process activity from one of the debugger sockets. This could be a new + * connection or a data packet. + */ + private void processDebuggerActivity(SelectionKey key) { + Debugger dbg = (Debugger)key.attachment(); + + try { + if (key.isAcceptable()) { + try { + acceptNewDebugger(dbg, null); + } catch (IOException ioe) { + Log.w("ddms", "debugger accept() failed"); + ioe.printStackTrace(); + } + } else if (key.isReadable()) { + processDebuggerData(key); + } else { + Log.d("ddm-debugger", "key in unknown state"); + } + } catch (CancelledKeyException cke) { + // key has been cancelled we can ignore that. + } + } + + /* + * Accept a new connection from a debugger. If successful, register it with + * the Selector. + */ + private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan) + throws IOException { + + synchronized (mClientList) { + SocketChannel chan; + + if (acceptChan == null) + chan = dbg.accept(); + else + chan = dbg.accept(acceptChan); + + if (chan != null) { + chan.socket().setTcpNoDelay(true); + + wakeup(); + + try { + chan.register(mSelector, SelectionKey.OP_READ, dbg); + } catch (IOException ioe) { + // failed, drop the connection + dbg.closeData(); + throw ioe; + } catch (RuntimeException re) { + // failed, drop the connection + dbg.closeData(); + throw re; + } + } else { + Log.w("ddms", "ignoring duplicate debugger"); + // new connection already closed + } + } + } + + /* + * We have incoming data from the debugger. Forward it to the client. + */ + private void processDebuggerData(SelectionKey key) { + Debugger dbg = (Debugger)key.attachment(); + + try { + /* + * Read pending data. + */ + dbg.read(); + + /* + * See if we have a full packet in the buffer. It's possible we have + * more than one packet, so we have to loop. + */ + JdwpPacket packet = dbg.getJdwpPacket(); + while (packet != null) { + Log.v("ddms", "Forwarding dbg req 0x" + + Integer.toHexString(packet.getId()) + " to " + + dbg.getClient()); + + dbg.forwardPacketToClient(packet); + + packet = dbg.getJdwpPacket(); + } + } catch (IOException ioe) { + /* + * Close data connection; automatically un-registers dbg from + * selector. The failure could be caused by the debugger going away, + * or by the client going away and failing to accept our data. + * Either way, the debugger connection does not need to exist any + * longer. We also need to recycle the connection to the client, so + * that the VM sees the debugger disconnect. For a DDM-aware client + * this won't be necessary, and we can just send a "debugger + * disconnected" message. + */ + Log.d("ddms", "Closing connection to debugger " + dbg); + dbg.closeData(); + Client client = dbg.getClient(); + if (client.isDdmAware()) { + // TODO: soft-disconnect DDM-aware clients + Log.d("ddms", " (recycling client connection as well)"); + + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client, + IDebugPortProvider.NO_STATIC_PORT); + } else { + Log.d("ddms", " (recycling client connection as well)"); + // we should drop the client, but also attempt to reopen it. + // This is done by the DeviceMonitor. + client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client, + IDebugPortProvider.NO_STATIC_PORT); + } + } + } + + /* + * Tell the thread that something has changed. + */ + private void wakeup() { + mSelector.wakeup(); + } + + /** + * Tell the thread to stop. Called from UI thread. + */ + synchronized void quit() { + mQuit = true; + wakeup(); + Log.d("ddms", "Waiting for Monitor thread"); + try { + this.join(); + // since we're quitting, lets drop all the client and disconnect + // the DebugSelectedPort + synchronized (mClientList) { + for (Client c : mClientList) { + c.close(false /* notify */); + broadcast(CLIENT_DISCONNECTED, c); + } + mClientList.clear(); + } + + if (mDebugSelectedChan != null) { + mDebugSelectedChan.close(); + mDebugSelectedChan.socket().close(); + mDebugSelectedChan = null; + } + mSelector.close(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + mInstance = null; + } + + /** + * Add a new Client to the list of things we monitor. Also adds the client's + * channel and the client's debugger listener to the selection list. This + * should only be called from one thread (the VMWatcherThread) to avoid a + * race between "alreadyOpen" and Client creation. + */ + synchronized void addClient(Client client) { + if (mInstance == null) { + return; + } + + Log.d("ddms", "Adding new client " + client); + + synchronized (mClientList) { + mClientList.add(client); + + /* + * Register the Client's socket channel with the selector. We attach + * the Client to the SelectionKey. If you try to register a new + * channel with the Selector while it is waiting for I/O, you will + * block. The solution is to call wakeup() and then hold a lock to + * ensure that the registration happens before the Selector goes + * back to sleep. + */ + try { + wakeup(); + + client.register(mSelector); + + Debugger dbg = client.getDebugger(); + if (dbg != null) { + dbg.registerListener(mSelector); + } + } catch (IOException ioe) { + // not really expecting this to happen + ioe.printStackTrace(); + } + } + } + + /* + * Broadcast an event to all message handlers. + */ + private void broadcast(int event, Client client) { + Log.d("ddms", "broadcast " + event + ": " + client); + + /* + * The handler objects appear once in mHandlerMap for each message they + * handle. We want to notify them once each, so we convert the HashMap + * to a HashSet before we iterate. + */ + HashSet set; + synchronized (mHandlerMap) { + Collection values = mHandlerMap.values(); + set = new HashSet(values); + } + + Iterator iter = set.iterator(); + while (iter.hasNext()) { + ChunkHandler handler = iter.next(); + switch (event) { + case CLIENT_READY: + try { + handler.clientReady(client); + } catch (IOException ioe) { + // Something failed with the client. It should + // fall out of the list the next time we try to + // do something with it, so we discard the + // exception here and assume cleanup will happen + // later. May need to propagate farther. The + // trouble is that not all values for "event" may + // actually throw an exception. + Log.w("ddms", + "Got exception while broadcasting 'ready'"); + return; + } + break; + case CLIENT_DISCONNECTED: + handler.clientDisconnected(client); + break; + default: + throw new UnsupportedOperationException(); + } + } + + } + + /** + * Opens (or reopens) the "debug selected" port and listen for connections. + * @return true if the port was opened successfully. + * @throws IOException + */ + private boolean reopenDebugSelectedPort() throws IOException { + + Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort); + if (mDebugSelectedChan != null) { + mDebugSelectedChan.close(); + } + + mDebugSelectedChan = ServerSocketChannel.open(); + mDebugSelectedChan.configureBlocking(false); // required for Selector + + InetSocketAddress addr = new InetSocketAddress( + InetAddress.getByName("localhost"), //$NON-NLS-1$ + mNewDebugSelectedPort); + mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR + + try { + mDebugSelectedChan.socket().bind(addr); + if (mSelectedClient != null) { + mSelectedClient.update(Client.CHANGE_PORT); + } + + mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this); + + return true; + } catch (java.net.BindException e) { + displayDebugSelectedBindError(mNewDebugSelectedPort); + + // do not attempt to reopen it. + mDebugSelectedChan = null; + mNewDebugSelectedPort = -1; + + return false; + } + } + + /* + * We have some activity on the "debug selected" port. Handle it. + */ + private void processDebugSelectedActivity(SelectionKey key) { + assert key.isAcceptable(); + + ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel(); + + /* + * Find the debugger associated with the currently-selected client. + */ + if (mSelectedClient != null) { + Debugger dbg = mSelectedClient.getDebugger(); + + if (dbg != null) { + Log.d("ddms", "Accepting connection on 'debug selected' port"); + try { + acceptNewDebugger(dbg, acceptChan); + } catch (IOException ioe) { + // client should be gone, keep going + } + + return; + } + } + + Log.w("ddms", + "Connection on 'debug selected' port, but none selected"); + try { + SocketChannel chan = acceptChan.accept(); + chan.close(); + } catch (IOException ioe) { + // not expected; client should be gone, keep going + } catch (NotYetBoundException e) { + displayDebugSelectedBindError(mDebugSelectedPort); + } + } + + private void displayDebugSelectedBindError(int port) { + String message = String.format( + "Could not open Selected VM debug port (%1$d). Make sure you do not have another instance of DDMS or of the eclipse plugin running. If it's being used by something else, choose a new port number in the preferences.", + port); + + Log.logAndDisplay(LogLevel.ERROR, "ddms", message); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java new file mode 100644 index 000000000..24dbb0544 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +/** + * Base implementation of {@link IShellOutputReceiver}, that takes the raw data coming from the + * socket, and convert it into {@link String} objects. + *

Additionally, it splits the string by lines. + *

Classes extending it must implement {@link #processNewLines(String[])} which receives + * new parsed lines as they become available. + */ +public abstract class MultiLineReceiver implements IShellOutputReceiver { + + private boolean mTrimLines = true; + + /** unfinished message line, stored for next packet */ + private String mUnfinishedLine = null; + + private final ArrayList mArray = new ArrayList(); + + /** + * Set the trim lines flag. + * @param trim hether the lines are trimmed, or not. + */ + public void setTrimLine(boolean trim) { + mTrimLines = trim; + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput( + * byte[], int, int) + */ + public final void addOutput(byte[] data, int offset, int length) { + if (isCancelled() == false) { + String s = null; + try { + s = new String(data, offset, length, "ISO-8859-1"); //$NON-NLS-1$ + } catch (UnsupportedEncodingException e) { + // normal encoding didn't work, try the default one + s = new String(data, offset,length); + } + + // ok we've got a string + if (s != null) { + // if we had an unfinished line we add it. + if (mUnfinishedLine != null) { + s = mUnfinishedLine + s; + mUnfinishedLine = null; + } + + // now we split the lines + mArray.clear(); + int start = 0; + do { + int index = s.indexOf("\r\n", start); //$NON-NLS-1$ + + // if \r\n was not found, this is an unfinished line + // and we store it to be processed for the next packet + if (index == -1) { + mUnfinishedLine = s.substring(start); + break; + } + + // so we found a \r\n; + // extract the line + String line = s.substring(start, index); + if (mTrimLines) { + line = line.trim(); + } + mArray.add(line); + + // move start to after the \r\n we found + start = index + 2; + } while (true); + + if (mArray.size() > 0) { + // at this point we've split all the lines. + // make the array + String[] lines = mArray.toArray(new String[mArray.size()]); + + // send it for final processing + processNewLines(lines); + } + } + } + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#flush() + */ + public final void flush() { + if (mUnfinishedLine != null) { + processNewLines(new String[] { mUnfinishedLine }); + } + + done(); + } + + /** + * Terminates the process. This is called after the last lines have been through + * {@link #processNewLines(String[])}. + */ + public void done() { + // do nothing. + } + + /** + * Called when new lines are being received by the remote process. + *

It is guaranteed that the lines are complete when they are given to this method. + * @param lines The array containing the new lines. + */ + public abstract void processNewLines(String[] lines); +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java new file mode 100644 index 000000000..956b004c0 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Stores native allocation information. + *

Contains number of allocations, their size and the stack trace. + *

Note: the ddmlib does not resolve the stack trace automatically. While this class provides + * storage for resolved stack trace, this is merely for convenience. + */ +public final class NativeAllocationInfo { + /* constants for flag bits */ + private static final int FLAG_ZYGOTE_CHILD = (1<<31); + private static final int FLAG_MASK = (FLAG_ZYGOTE_CHILD); + + /** + * list of alloc functions that are filtered out when attempting to display + * a relevant method responsible for an allocation + */ + private static ArrayList sAllocFunctionFilter; + static { + sAllocFunctionFilter = new ArrayList(); + sAllocFunctionFilter.add("malloc"); //$NON-NLS-1$ + sAllocFunctionFilter.add("calloc"); //$NON-NLS-1$ + sAllocFunctionFilter.add("realloc"); //$NON-NLS-1$ + sAllocFunctionFilter.add("get_backtrace"); //$NON-NLS-1$ + sAllocFunctionFilter.add("get_hash"); //$NON-NLS-1$ + sAllocFunctionFilter.add("??"); //$NON-NLS-1$ + sAllocFunctionFilter.add("internal_free"); //$NON-NLS-1$ + sAllocFunctionFilter.add("operator new"); //$NON-NLS-1$ + sAllocFunctionFilter.add("leak_free"); //$NON-NLS-1$ + sAllocFunctionFilter.add("chk_free"); //$NON-NLS-1$ + sAllocFunctionFilter.add("chk_memalign"); //$NON-NLS-1$ + sAllocFunctionFilter.add("Malloc"); //$NON-NLS-1$ + } + + private final int mSize; + + private final boolean mIsZygoteChild; + + private final int mAllocations; + + private final ArrayList mStackCallAddresses = new ArrayList(); + + private ArrayList mResolvedStackCall = null; + + private boolean mIsStackCallResolved = false; + + /** + * Constructs a new {@link NativeAllocationInfo}. + * @param size The size of the allocations. + * @param allocations the allocation count + */ + NativeAllocationInfo(int size, int allocations) { + this.mSize = size & ~FLAG_MASK; + this.mIsZygoteChild = ((size & FLAG_ZYGOTE_CHILD) != 0); + this.mAllocations = allocations; + } + + /** + * Adds a stack call address for this allocation. + * @param address The address to add. + */ + void addStackCallAddress(long address) { + mStackCallAddresses.add(address); + } + + /** + * Returns the total size of this allocation. + */ + public int getSize() { + return mSize; + } + + /** + * Returns whether the allocation happened in a child of the zygote + * process. + */ + public boolean isZygoteChild() { + return mIsZygoteChild; + } + + /** + * Returns the allocation count. + */ + public int getAllocationCount() { + return mAllocations; + } + + /** + * Returns whether the stack call addresses have been resolved into + * {@link NativeStackCallInfo} objects. + */ + public boolean isStackCallResolved() { + return mIsStackCallResolved; + } + + /** + * Returns the stack call of this allocation as raw addresses. + * @return the list of addresses where the allocation happened. + */ + public Long[] getStackCallAddresses() { + return mStackCallAddresses.toArray(new Long[mStackCallAddresses.size()]); + } + + /** + * Sets the resolved stack call for this allocation. + *

+ * If resolvedStackCall is non null then + * {@link #isStackCallResolved()} will return true after this call. + * @param resolvedStackCall The list of {@link NativeStackCallInfo}. + */ + public synchronized void setResolvedStackCall(List resolvedStackCall) { + if (mResolvedStackCall == null) { + mResolvedStackCall = new ArrayList(); + } else { + mResolvedStackCall.clear(); + } + mResolvedStackCall.addAll(resolvedStackCall); + mIsStackCallResolved = mResolvedStackCall.size() != 0; + } + + /** + * Returns the resolved stack call. + * @return An array of {@link NativeStackCallInfo} or null if the stack call + * was not resolved. + * @see #setResolvedStackCall(ArrayList) + * @see #isStackCallResolved() + */ + public synchronized NativeStackCallInfo[] getResolvedStackCall() { + if (mIsStackCallResolved) { + return mResolvedStackCall.toArray(new NativeStackCallInfo[mResolvedStackCall.size()]); + } + + return null; + } + + /** + * Indicates whether some other object is "equal to" this one. + * @param obj the reference object with which to compare. + * @return true if this object is equal to the obj argument; + * false otherwise. + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof NativeAllocationInfo) { + NativeAllocationInfo mi = (NativeAllocationInfo)obj; + // quick compare of size, alloc, and stackcall size + if (mSize != mi.mSize || mAllocations != mi.mAllocations || + mStackCallAddresses.size() != mi.mStackCallAddresses.size()) { + return false; + } + // compare the stack addresses + int count = mStackCallAddresses.size(); + for (int i = 0 ; i < count ; i++) { + long a = mStackCallAddresses.get(i); + long b = mi.mStackCallAddresses.get(i); + if (a != b) { + return false; + } + } + + return true; + } + return false; + } + + /** + * Returns a string representation of the object. + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("Allocations: "); + buffer.append(mAllocations); + buffer.append("\n"); //$NON-NLS-1$ + + buffer.append("Size: "); + buffer.append(mSize); + buffer.append("\n"); //$NON-NLS-1$ + + buffer.append("Total Size: "); + buffer.append(mSize * mAllocations); + buffer.append("\n"); //$NON-NLS-1$ + + Iterator addrIterator = mStackCallAddresses.iterator(); + Iterator sourceIterator = mResolvedStackCall.iterator(); + + while (sourceIterator.hasNext()) { + long addr = addrIterator.next(); + NativeStackCallInfo source = sourceIterator.next(); + if (addr == 0) + continue; + + if (source.getLineNumber() != -1) { + buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s:%5$d\n", addr, + source.getLibraryName(), source.getMethodName(), + source.getSourceFile(), source.getLineNumber())); + } else { + buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s\n", addr, + source.getLibraryName(), source.getMethodName(), source.getSourceFile())); + } + } + + return buffer.toString(); + } + + /** + * Returns the first {@link NativeStackCallInfo} that is relevant. + *

+ * A relevant NativeStackCallInfo is a stack call that is not deep in the + * lower level of the libc, but the actual method that performed the allocation. + * @return a NativeStackCallInfo or null if the stack call has not + * been processed from the raw addresses. + * @see #setResolvedStackCall(ArrayList) + * @see #isStackCallResolved() + */ + public synchronized NativeStackCallInfo getRelevantStackCallInfo() { + if (mIsStackCallResolved && mResolvedStackCall != null) { + Iterator sourceIterator = mResolvedStackCall.iterator(); + Iterator addrIterator = mStackCallAddresses.iterator(); + + while (sourceIterator.hasNext() && addrIterator.hasNext()) { + long addr = addrIterator.next(); + NativeStackCallInfo info = sourceIterator.next(); + if (addr != 0 && info != null) { + if (isRelevant(info.getMethodName())) { + return info; + } + } + } + + // couldnt find a relevant one, so we'll return the first one if it + // exists. + if (mResolvedStackCall.size() > 0) + return mResolvedStackCall.get(0); + } + + return null; + } + + /** + * Returns true if the method name is relevant. + * @param methodName the method name to test. + */ + private boolean isRelevant(String methodName) { + for (String filter : sAllocFunctionFilter) { + if (methodName.contains(filter)) { + return false; + } + } + + return true; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java new file mode 100644 index 000000000..5a2631740 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +/** + * Memory address to library mapping for native libraries. + *

+ * Each instance represents a single native library and its start and end memory addresses. + */ +public final class NativeLibraryMapInfo { + private long mStartAddr; + private long mEndAddr; + + private String mLibrary; + + /** + * Constructs a new native library map info. + * @param startAddr The start address of the library. + * @param endAddr The end address of the library. + * @param library The name of the library. + */ + NativeLibraryMapInfo(long startAddr, long endAddr, String library) { + this.mStartAddr = startAddr; + this.mEndAddr = endAddr; + this.mLibrary = library; + } + + /** + * Returns the name of the library. + */ + public String getLibraryName() { + return mLibrary; + } + + /** + * Returns the start address of the library. + */ + public long getStartAddress() { + return mStartAddr; + } + + /** + * Returns the end address of the library. + */ + public long getEndAddress() { + return mEndAddr; + } + + /** + * Returns whether the specified address is inside the library. + * @param address The address to test. + * @return true if the address is between the start and end address of the library. + * @see #getStartAddress() + * @see #getEndAddress() + */ + public boolean isWithinLibrary(long address) { + return address >= mStartAddr && address <= mEndAddr; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java new file mode 100644 index 000000000..e54818d58 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents a stack call. This is used to return all of the call + * information as one object. + */ +public final class NativeStackCallInfo { + private final static Pattern SOURCE_NAME_PATTERN = Pattern.compile("^(.+):(\\d+)$"); + + /** name of the library */ + private String mLibrary; + + /** name of the method */ + private String mMethod; + + /** + * name of the source file + line number in the format
+ * <sourcefile>:<linenumber> + */ + private String mSourceFile; + + private int mLineNumber = -1; + + /** + * Basic constructor with library, method, and sourcefile information + * + * @param lib The name of the library + * @param method the name of the method + * @param sourceFile the name of the source file and the line number + * as "[sourcefile]:[fileNumber]" + */ + public NativeStackCallInfo(String lib, String method, String sourceFile) { + mLibrary = lib; + mMethod = method; + + Matcher m = SOURCE_NAME_PATTERN.matcher(sourceFile); + if (m.matches()) { + mSourceFile = m.group(1); + try { + mLineNumber = Integer.parseInt(m.group(2)); + } catch (NumberFormatException e) { + // do nothing, the line number will stay at -1 + } + } else { + mSourceFile = sourceFile; + } + } + + /** + * Returns the name of the library name. + */ + public String getLibraryName() { + return mLibrary; + } + + /** + * Returns the name of the method. + */ + public String getMethodName() { + return mMethod; + } + + /** + * Returns the name of the source file. + */ + public String getSourceFile() { + return mSourceFile; + } + + /** + * Returns the line number, or -1 if unknown. + */ + public int getLineNumber() { + return mLineNumber; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java new file mode 100644 index 000000000..d2b5a1efd --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +/** + * Implementation of {@link IShellOutputReceiver} that does nothing. + *

This can be used to execute a remote shell command when the output is not needed. + */ +public final class NullOutputReceiver implements IShellOutputReceiver { + + private static NullOutputReceiver sReceiver = new NullOutputReceiver(); + + public static IShellOutputReceiver getReceiver() { + return sReceiver; + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(byte[], int, int) + */ + public void addOutput(byte[] data, int offset, int length) { + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#flush() + */ + public void flush() { + } + + /* (non-Javadoc) + * @see com.android.ddmlib.adb.IShellOutputReceiver#isCancelled() + */ + public boolean isCancelled() { + return false; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java new file mode 100644 index 000000000..3ec61485e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import java.nio.ByteBuffer; + +/** + * Data representing an image taken from a device frame buffer. + */ +public final class RawImage { + public int version; + public int bpp; + public int size; + public int width; + public int height; + public int red_offset; + public int red_length; + public int blue_offset; + public int blue_length; + public int green_offset; + public int green_length; + public int alpha_offset; + public int alpha_length; + + public byte[] data; + + /** + * Reads the header of a RawImage from a {@link ByteBuffer}. + *

The way the data is sent over adb is defined in system/core/adb/framebuffer_service.c + * @param version the version of the protocol. + * @param buf the buffer to read from. + * @return true if success + */ + public boolean readHeader(int version, ByteBuffer buf) { + this.version = version; + + if (version == 16) { + // compatibility mode with original protocol + this.bpp = 16; + + // read actual values. + this.size = buf.getInt(); + this.width = buf.getInt(); + this.height = buf.getInt(); + + // create default values for the rest. Format is 565 + this.red_offset = 11; + this.red_length = 5; + this.green_offset = 5; + this.green_length = 6; + this.blue_offset = 0; + this.blue_length = 5; + this.alpha_offset = 0; + this.alpha_length = 0; + } else if (version == 1) { + this.bpp = buf.getInt(); + this.size = buf.getInt(); + this.width = buf.getInt(); + this.height = buf.getInt(); + this.red_offset = buf.getInt(); + this.red_length = buf.getInt(); + this.blue_offset = buf.getInt(); + this.blue_length = buf.getInt(); + this.green_offset = buf.getInt(); + this.green_length = buf.getInt(); + this.alpha_offset = buf.getInt(); + this.alpha_length = buf.getInt(); + } else { + // unsupported protocol! + return false; + } + + return true; + } + + /** + * Returns the mask value for the red color. + *

This value is compatible with org.eclipse.swt.graphics.PaletteData + */ + public int getRedMask() { + return getMask(red_length, red_offset); + } + + /** + * Returns the mask value for the green color. + *

This value is compatible with org.eclipse.swt.graphics.PaletteData + */ + public int getGreenMask() { + return getMask(green_length, green_offset); + } + + /** + * Returns the mask value for the blue color. + *

This value is compatible with org.eclipse.swt.graphics.PaletteData + */ + public int getBlueMask() { + return getMask(blue_length, blue_offset); + } + + /** + * Returns the size of the header for a specific version of the framebuffer adb protocol. + * @param version the version of the protocol + * @return the number of int that makes up the header. + */ + public static int getHeaderSize(int version) { + switch (version) { + case 16: // compatibility mode + return 3; // size, width, height + case 1: + return 12; // bpp, size, width, height, 4*(length, offset) + } + + return 0; + } + + /** + * Returns a rotated version of the image + * The image is rotated counter-clockwise. + */ + public RawImage getRotated() { + RawImage rotated = new RawImage(); + rotated.version = this.version; + rotated.bpp = this.bpp; + rotated.size = this.size; + rotated.red_offset = this.red_offset; + rotated.red_length = this.red_length; + rotated.blue_offset = this.blue_offset; + rotated.blue_length = this.blue_length; + rotated.green_offset = this.green_offset; + rotated.green_length = this.green_length; + rotated.alpha_offset = this.alpha_offset; + rotated.alpha_length = this.alpha_length; + + rotated.width = this.height; + rotated.height = this.width; + + int count = this.data.length; + rotated.data = new byte[count]; + + int byteCount = this.bpp >> 3; // bpp is in bits, we want bytes to match our array + final int w = this.width; + final int h = this.height; + for (int y = 0 ; y < h ; y++) { + for (int x = 0 ; x < w ; x++) { + System.arraycopy( + this.data, (y * w + x) * byteCount, + rotated.data, ((w-x-1) * h + y) * byteCount, + byteCount); + } + } + + return rotated; + } + + /** + * Returns an ARGB integer value for the pixel at index in {@link #data}. + */ + public int getARGB(int index) { + int value; + if (bpp == 16) { + value = data[index] & 0x00FF; + value |= (data[index+1] << 8) & 0x0FF00; + } else if (bpp == 32) { + value = data[index] & 0x00FF; + value |= (data[index+1] & 0x00FF) << 8; + value |= (data[index+2] & 0x00FF) << 16; + value |= (data[index+3] & 0x00FF) << 24; + } else { + throw new UnsupportedOperationException("RawImage.getARGB(int) only works in 16 and 32 bit mode."); + } + + int r = ((value >>> red_offset) & getMask(red_length)) << (8 - red_length); + int g = ((value >>> green_offset) & getMask(green_length)) << (8 - green_length); + int b = ((value >>> blue_offset) & getMask(blue_length)) << (8 - blue_length); + int a; + if (alpha_length == 0) { + a = 0xFF; // force alpha to opaque if there's no alpha value in the framebuffer. + } else { + a = ((value >>> alpha_offset) & getMask(alpha_length)) << (8 - alpha_length); + } + + return a << 24 | r << 16 | g << 8 | b; + } + + /** + * creates a mask value based on a length and offset. + *

This value is compatible with org.eclipse.swt.graphics.PaletteData + */ + private int getMask(int length, int offset) { + int res = getMask(length) << offset; + + // if the bpp is 32 bits then we need to invert it because the buffer is in little endian + if (bpp == 32) { + return Integer.reverseBytes(res); + } + + return res; + } + + /** + * Creates a mask value based on a length. + * @param length + * @return + */ + private int getMask(int length) { + int res = 0; + for (int i = 0 ; i < length ; i++) { + res = (res << 1) + 1; + } + + return res; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java new file mode 100644 index 000000000..9f6b56149 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java @@ -0,0 +1,980 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +import com.android.ddmlib.AdbHelper.AdbResponse; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.utils.ArrayHelper; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; + +/** + * Sync service class to push/pull to/from devices/emulators, through the debug bridge. + *

+ * To get a {@link SyncService} object, use {@link Device#getSyncService()}. + */ +public final class SyncService { + + private final static byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' }; + private final static byte[] ID_FAIL = { 'F', 'A', 'I', 'L' }; + private final static byte[] ID_STAT = { 'S', 'T', 'A', 'T' }; + private final static byte[] ID_RECV = { 'R', 'E', 'C', 'V' }; + private final static byte[] ID_DATA = { 'D', 'A', 'T', 'A' }; + private final static byte[] ID_DONE = { 'D', 'O', 'N', 'E' }; + private final static byte[] ID_SEND = { 'S', 'E', 'N', 'D' }; +// private final static byte[] ID_LIST = { 'L', 'I', 'S', 'T' }; +// private final static byte[] ID_DENT = { 'D', 'E', 'N', 'T' }; + + private final static NullSyncProgresMonitor sNullSyncProgressMonitor = + new NullSyncProgresMonitor(); + + private final static int S_ISOCK = 0xC000; // type: symbolic link + private final static int S_IFLNK = 0xA000; // type: symbolic link + private final static int S_IFREG = 0x8000; // type: regular file + private final static int S_IFBLK = 0x6000; // type: block device + private final static int S_IFDIR = 0x4000; // type: directory + private final static int S_IFCHR = 0x2000; // type: character device + private final static int S_IFIFO = 0x1000; // type: fifo +/* + private final static int S_ISUID = 0x0800; // set-uid bit + private final static int S_ISGID = 0x0400; // set-gid bit + private final static int S_ISVTX = 0x0200; // sticky bit + private final static int S_IRWXU = 0x01C0; // user permissions + private final static int S_IRUSR = 0x0100; // user: read + private final static int S_IWUSR = 0x0080; // user: write + private final static int S_IXUSR = 0x0040; // user: execute + private final static int S_IRWXG = 0x0038; // group permissions + private final static int S_IRGRP = 0x0020; // group: read + private final static int S_IWGRP = 0x0010; // group: write + private final static int S_IXGRP = 0x0008; // group: execute + private final static int S_IRWXO = 0x0007; // other permissions + private final static int S_IROTH = 0x0004; // other: read + private final static int S_IWOTH = 0x0002; // other: write + private final static int S_IXOTH = 0x0001; // other: execute +*/ + + private final static int SYNC_DATA_MAX = 64*1024; + private final static int REMOTE_PATH_MAX_LENGTH = 1024; + + /** Result code for transfer success. */ + public static final int RESULT_OK = 0; + /** Result code for canceled transfer */ + public static final int RESULT_CANCELED = 1; + /** Result code for unknown error */ + public static final int RESULT_UNKNOWN_ERROR = 2; + /** Result code for network connection error */ + public static final int RESULT_CONNECTION_ERROR = 3; + /** Result code for unknown remote object during a pull */ + public static final int RESULT_NO_REMOTE_OBJECT = 4; + /** Result code when attempting to pull multiple files into a file */ + public static final int RESULT_TARGET_IS_FILE = 5; + /** Result code when attempting to pull multiple into a directory that does not exist. */ + public static final int RESULT_NO_DIR_TARGET = 6; + /** Result code for wrong encoding on the remote path. */ + public static final int RESULT_REMOTE_PATH_ENCODING = 7; + /** Result code for remote path that is too long. */ + public static final int RESULT_REMOTE_PATH_LENGTH = 8; + /** Result code for error while writing local file. */ + public static final int RESULT_FILE_WRITE_ERROR = 9; + /** Result code for error while reading local file. */ + public static final int RESULT_FILE_READ_ERROR = 10; + /** Result code for attempting to push a file that does not exist. */ + public static final int RESULT_NO_LOCAL_FILE = 11; + /** Result code for attempting to push a directory. */ + public static final int RESULT_LOCAL_IS_DIRECTORY = 12; + /** Result code for when the target path of a multi file push is a file. */ + public static final int RESULT_REMOTE_IS_FILE = 13; + /** Result code for receiving too much data from the remove device at once */ + public static final int RESULT_BUFFER_OVERRUN = 14; + + /** + * A file transfer result. + *

+ * This contains a code, and an optional string + */ + public static class SyncResult { + private int mCode; + private String mMessage; + SyncResult(int code, String message) { + mCode = code; + mMessage = message; + } + + SyncResult(int code, Exception e) { + this(code, e.getMessage()); + } + + SyncResult(int code) { + this(code, errorCodeToString(code)); + } + + public int getCode() { + return mCode; + } + + public String getMessage() { + return mMessage; + } + } + + /** + * Classes which implement this interface provide methods that deal + * with displaying transfer progress. + */ + public interface ISyncProgressMonitor { + /** + * Sent when the transfer starts + * @param totalWork the total amount of work. + */ + public void start(int totalWork); + /** + * Sent when the transfer is finished or interrupted. + */ + public void stop(); + /** + * Sent to query for possible cancellation. + * @return true if the transfer should be stopped. + */ + public boolean isCanceled(); + /** + * Sent when a sub task is started. + * @param name the name of the sub task. + */ + public void startSubTask(String name); + /** + * Sent when some progress have been made. + * @param work the amount of work done. + */ + public void advance(int work); + } + + /** + * A Sync progress monitor that does nothing + */ + private static class NullSyncProgresMonitor implements ISyncProgressMonitor { + public void advance(int work) { + } + public boolean isCanceled() { + return false; + } + + public void start(int totalWork) { + } + public void startSubTask(String name) { + } + public void stop() { + } + } + + private InetSocketAddress mAddress; + private Device mDevice; + private SocketChannel mChannel; + + /** + * Buffer used to send data. Allocated when needed and reused afterward. + */ + private byte[] mBuffer; + + /** + * Creates a Sync service object. + * @param address The address to connect to + * @param device the {@link Device} that the service connects to. + */ + SyncService(InetSocketAddress address, Device device) { + mAddress = address; + mDevice = device; + } + + /** + * Opens the sync connection. This must be called before any calls to push[File] / pull[File]. + * @return true if the connection opened, false if adb refuse the connection. This can happen + * if the {@link Device} is invalid. + * @throws IOException If the connection to adb failed. + */ + boolean openSync() throws IOException { + try { + mChannel = SocketChannel.open(mAddress); + mChannel.configureBlocking(false); + + // target a specific device + AdbHelper.setDevice(mChannel, mDevice); + + byte[] request = AdbHelper.formAdbRequest("sync:"); // $NON-NLS-1$ + AdbHelper.write(mChannel, request, -1, DdmPreferences.getTimeOut()); + + AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */); + + if (!resp.ioSuccess || !resp.okay) { + Log.w("ddms", + "Got timeout or unhappy response from ADB sync req: " + + resp.message); + mChannel.close(); + mChannel = null; + return false; + } + } catch (IOException e) { + if (mChannel != null) { + try { + mChannel.close(); + } catch (IOException e2) { + // we want to throw the original exception, so we ignore this one. + } + mChannel = null; + } + + throw e; + } + + return true; + } + + /** + * Closes the connection. + */ + public void close() { + if (mChannel != null) { + try { + mChannel.close(); + } catch (IOException e) { + // nothing to be done really... + } + mChannel = null; + } + } + + /** + * Returns a sync progress monitor that does nothing. This allows background tasks that don't + * want/need to display ui, to pass a valid {@link ISyncProgressMonitor}. + *

This object can be reused multiple times and can be used by concurrent threads. + */ + public static ISyncProgressMonitor getNullProgressMonitor() { + return sNullSyncProgressMonitor; + } + + /** + * Converts an error code into a non-localized string + * @param code the error code; + */ + private static String errorCodeToString(int code) { + switch (code) { + case RESULT_OK: + return "Success."; + case RESULT_CANCELED: + return "Tranfert canceled by the user."; + case RESULT_UNKNOWN_ERROR: + return "Unknown Error."; + case RESULT_CONNECTION_ERROR: + return "Adb Connection Error."; + case RESULT_NO_REMOTE_OBJECT: + return "Remote object doesn't exist!"; + case RESULT_TARGET_IS_FILE: + return "Target object is a file."; + case RESULT_NO_DIR_TARGET: + return "Target directory doesn't exist."; + case RESULT_REMOTE_PATH_ENCODING: + return "Remote Path encoding is not supported."; + case RESULT_REMOTE_PATH_LENGTH: + return "Remove path is too long."; + case RESULT_FILE_WRITE_ERROR: + return "Writing local file failed!"; + case RESULT_FILE_READ_ERROR: + return "Reading local file failed!"; + case RESULT_NO_LOCAL_FILE: + return "Local file doesn't exist."; + case RESULT_LOCAL_IS_DIRECTORY: + return "Local path is a directory."; + case RESULT_REMOTE_IS_FILE: + return "Remote path is a file."; + case RESULT_BUFFER_OVERRUN: + return "Receiving too much data."; + } + + throw new RuntimeException(); + } + + /** + * Pulls file(s) or folder(s). + * @param entries the remote item(s) to pull + * @param localPath The local destination. If the entries count is > 1 or + * if the unique entry is a folder, this should be a folder. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public SyncResult pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor) { + + // first we check the destination is a directory and exists + File f = new File(localPath); + if (f.exists() == false) { + return new SyncResult(RESULT_NO_DIR_TARGET); + } + if (f.isDirectory() == false) { + return new SyncResult(RESULT_TARGET_IS_FILE); + } + + // get a FileListingService object + FileListingService fls = new FileListingService(mDevice); + + // compute the number of file to move + int total = getTotalRemoteFileSize(entries, fls); + + // start the monitor + monitor.start(total); + + SyncResult result = doPull(entries, localPath, fls, monitor); + + monitor.stop(); + + return result; + } + + /** + * Pulls a single file. + * @param remote the remote file + * @param localFilename The local destination. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public SyncResult pullFile(FileEntry remote, String localFilename, + ISyncProgressMonitor monitor) { + int total = remote.getSizeValue(); + monitor.start(total); + + SyncResult result = doPullFile(remote.getFullPath(), localFilename, monitor); + + monitor.stop(); + return result; + } + + /** + * Pulls a single file. + *

Because this method just deals with a String for the remote file instead of a + * {@link FileEntry}, the size of the file being pulled is unknown and the + * {@link ISyncProgressMonitor} will not properly show the progress + * @param remoteFilepath the full path to the remote file + * @param localFilename The local destination. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see #getNullProgressMonitor() + */ + public SyncResult pullFile(String remoteFilepath, String localFilename, + ISyncProgressMonitor monitor) { + monitor.start(0); + //TODO: use the {@link FileListingService} to get the file size. + + SyncResult result = doPullFile(remoteFilepath, localFilename, monitor); + + monitor.stop(); + return result; + } + + /** + * Push several files. + * @param local An array of loca files to push + * @param remote the remote {@link FileEntry} representing a directory. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + */ + public SyncResult push(String[] local, FileEntry remote, ISyncProgressMonitor monitor) { + if (remote.isDirectory() == false) { + return new SyncResult(RESULT_REMOTE_IS_FILE); + } + + // make a list of File from the list of String + ArrayList files = new ArrayList(); + for (String path : local) { + files.add(new File(path)); + } + + // get the total count of the bytes to transfer + File[] fileArray = files.toArray(new File[files.size()]); + int total = getTotalLocalFileSize(fileArray); + + monitor.start(total); + + SyncResult result = doPush(fileArray, remote.getFullPath(), monitor); + + monitor.stop(); + + return result; + } + + /** + * Push a single file. + * @param local the local filepath. + * @param remote The remote filepath. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + */ + public SyncResult pushFile(String local, String remote, ISyncProgressMonitor monitor) { + File f = new File(local); + if (f.exists() == false) { + return new SyncResult(RESULT_NO_LOCAL_FILE); + } + + if (f.isDirectory()) { + return new SyncResult(RESULT_LOCAL_IS_DIRECTORY); + } + + monitor.start((int)f.length()); + + SyncResult result = doPushFile(local, remote, monitor); + + monitor.stop(); + + return result; + } + + /** + * compute the recursive file size of all the files in the list. Folder + * have a weight of 1. + * @param entries + * @param fls + * @return + */ + private int getTotalRemoteFileSize(FileEntry[] entries, FileListingService fls) { + int count = 0; + for (FileEntry e : entries) { + int type = e.getType(); + if (type == FileListingService.TYPE_DIRECTORY) { + // get the children + FileEntry[] children = fls.getChildren(e, false, null); + count += getTotalRemoteFileSize(children, fls) + 1; + } else if (type == FileListingService.TYPE_FILE) { + count += e.getSizeValue(); + } + } + + return count; + } + + /** + * compute the recursive file size of all the files in the list. Folder + * have a weight of 1. + * This does not check for circular links. + * @param files + * @return + */ + private int getTotalLocalFileSize(File[] files) { + int count = 0; + + for (File f : files) { + if (f.exists()) { + if (f.isDirectory()) { + return getTotalLocalFileSize(f.listFiles()) + 1; + } else if (f.isFile()) { + count += f.length(); + } + } + } + + return count; + } + + /** + * Pulls multiple files/folders recursively. + * @param entries The list of entry to pull + * @param localPath the localpath to a directory + * @param fileListingService a FileListingService object to browse through remote directories. + * @param monitor the progress monitor. Must be started already. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPull(FileEntry[] entries, String localPath, + FileListingService fileListingService, + ISyncProgressMonitor monitor) { + + for (FileEntry e : entries) { + // check if we're cancelled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // get type (we only pull directory and files for now) + int type = e.getType(); + if (type == FileListingService.TYPE_DIRECTORY) { + monitor.startSubTask(e.getFullPath()); + String dest = localPath + File.separator + e.getName(); + + // make the directory + File d = new File(dest); + d.mkdir(); + + // then recursively call the content. Since we did a ls command + // to get the number of files, we can use the cache + FileEntry[] children = fileListingService.getChildren(e, true, null); + SyncResult result = doPull(children, dest, fileListingService, monitor); + if (result.mCode != RESULT_OK) { + return result; + } + monitor.advance(1); + } else if (type == FileListingService.TYPE_FILE) { + monitor.startSubTask(e.getFullPath()); + String dest = localPath + File.separator + e.getName(); + SyncResult result = doPullFile(e.getFullPath(), dest, monitor); + if (result.mCode != RESULT_OK) { + return result; + } + } + } + + return new SyncResult(RESULT_OK); + } + + /** + * Pulls a remote file + * @param remotePath the remote file (length max is 1024) + * @param localPath the local destination + * @param monitor the monitor. The monitor must be started already. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPullFile(String remotePath, String localPath, + ISyncProgressMonitor monitor) { + byte[] msg = null; + byte[] pullResult = new byte[8]; + + final int timeOut = DdmPreferences.getTimeOut(); + + try { + byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING); + + if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { + return new SyncResult(RESULT_REMOTE_PATH_LENGTH); + } + + // create the full request message + msg = createFileReq(ID_RECV, remotePathContent); + + // and send it. + AdbHelper.write(mChannel, msg, -1, timeOut); + + // read the result, in a byte array containing 2 ints + // (id, size) + AdbHelper.read(mChannel, pullResult, -1, timeOut); + + // check we have the proper data back + if (checkResult(pullResult, ID_DATA) == false && + checkResult(pullResult, ID_DONE) == false) { + return new SyncResult(RESULT_CONNECTION_ERROR); + } + } catch (UnsupportedEncodingException e) { + return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // access the destination file + File f = new File(localPath); + + // create the stream to write in the file. We use a new try/catch block to differentiate + // between file and network io exceptions. + FileOutputStream fos = null; + try { + fos = new FileOutputStream(f); + } catch (FileNotFoundException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + + // the buffer to read the data + byte[] data = new byte[SYNC_DATA_MAX]; + + // loop to get data until we're done. + while (true) { + // check if we're cancelled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // if we're done, we stop the loop + if (checkResult(pullResult, ID_DONE)) { + break; + } + if (checkResult(pullResult, ID_DATA) == false) { + // hmm there's an error + return new SyncResult(RESULT_CONNECTION_ERROR); + } + int length = ArrayHelper.swap32bitFromArray(pullResult, 4); + if (length > SYNC_DATA_MAX) { + // buffer overrun! + // error and exit + return new SyncResult(RESULT_BUFFER_OVERRUN); + } + + try { + // now read the length we received + AdbHelper.read(mChannel, data, length, timeOut); + + // get the header for the next packet. + AdbHelper.read(mChannel, pullResult, -1, timeOut); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // write the content in the file + try { + fos.write(data, 0, length); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + + monitor.advance(length); + } + + try { + fos.flush(); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + return new SyncResult(RESULT_OK); + } + + + /** + * Push multiple files + * @param fileArray + * @param remotePath + * @param monitor + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor) { + for (File f : fileArray) { + // check if we're canceled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + if (f.exists()) { + if (f.isDirectory()) { + // append the name of the directory to the remote path + String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S + monitor.startSubTask(dest); + SyncResult result = doPush(f.listFiles(), dest, monitor); + + if (result.mCode != RESULT_OK) { + return result; + } + + monitor.advance(1); + } else if (f.isFile()) { + // append the name of the file to the remote path + String remoteFile = remotePath + "/" + f.getName(); // $NON-NLS-1S + monitor.startSubTask(remoteFile); + SyncResult result = doPushFile(f.getAbsolutePath(), remoteFile, monitor); + if (result.mCode != RESULT_OK) { + return result; + } + } + } + } + + return new SyncResult(RESULT_OK); + } + + /** + * Push a single file + * @param localPath the local file to push + * @param remotePath the remote file (length max is 1024) + * @param monitor the monitor. The monitor must be started already. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPushFile(String localPath, String remotePath, + ISyncProgressMonitor monitor) { + FileInputStream fis = null; + byte[] msg; + + final int timeOut = DdmPreferences.getTimeOut(); + + try { + byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING); + + if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { + return new SyncResult(RESULT_REMOTE_PATH_LENGTH); + } + + File f = new File(localPath); + + // this shouldn't happen but still... + if (f.exists() == false) { + return new SyncResult(RESULT_NO_LOCAL_FILE); + } + + // create the stream to read the file + fis = new FileInputStream(f); + + // create the header for the action + msg = createSendFileReq(ID_SEND, remotePathContent, 0644); + } catch (UnsupportedEncodingException e) { + return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e); + } catch (FileNotFoundException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + // and send it. We use a custom try/catch block to make the difference between + // file and network IO exceptions. + try { + AdbHelper.write(mChannel, msg, -1, timeOut); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // create the buffer used to read. + // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning. + if (mBuffer == null) { + mBuffer = new byte[SYNC_DATA_MAX + 8]; + } + System.arraycopy(ID_DATA, 0, mBuffer, 0, ID_DATA.length); + + // look while there is something to read + while (true) { + // check if we're canceled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // read up to SYNC_DATA_MAX + int readCount = 0; + try { + readCount = fis.read(mBuffer, 8, SYNC_DATA_MAX); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + if (readCount == -1) { + // we reached the end of the file + break; + } + + // now send the data to the device + // first write the amount read + ArrayHelper.swap32bitsToArray(readCount, mBuffer, 4); + + // now write it + try { + AdbHelper.write(mChannel, mBuffer, readCount+8, timeOut); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // and advance the monitor + monitor.advance(readCount); + } + // close the local file + try { + fis.close(); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + try { + // create the DONE message + long time = System.currentTimeMillis() / 1000; + msg = createReq(ID_DONE, (int)time); + + // and send it. + AdbHelper.write(mChannel, msg, -1, timeOut); + + // read the result, in a byte array containing 2 ints + // (id, size) + byte[] result = new byte[8]; + AdbHelper.read(mChannel, result, -1 /* full length */, timeOut); + + if (checkResult(result, ID_OKAY) == false) { + if (checkResult(result, ID_FAIL)) { + // read some error message... + int len = ArrayHelper.swap32bitFromArray(result, 4); + + AdbHelper.read(mChannel, mBuffer, len, timeOut); + + // output the result? + String message = new String(mBuffer, 0, len); + Log.e("ddms", "transfer error: " + message); + return new SyncResult(RESULT_UNKNOWN_ERROR, message); + } + + return new SyncResult(RESULT_UNKNOWN_ERROR); + } + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + return new SyncResult(RESULT_OK); + } + + /** + * Returns the mode of the remote file. + * @param path the remote file + * @return and Integer containing the mode if all went well or null + * otherwise + */ + private Integer readMode(String path) { + try { + // create the stat request message. + byte[] msg = createFileReq(ID_STAT, path); + + AdbHelper.write(mChannel, msg, -1 /* full length */, DdmPreferences.getTimeOut()); + + // read the result, in a byte array containing 4 ints + // (id, mode, size, time) + byte[] statResult = new byte[16]; + AdbHelper.read(mChannel, statResult, -1 /* full length */, DdmPreferences.getTimeOut()); + + // check we have the proper data back + if (checkResult(statResult, ID_STAT) == false) { + return null; + } + + // we return the mode (2nd int in the array) + return ArrayHelper.swap32bitFromArray(statResult, 4); + } catch (IOException e) { + return null; + } + } + + /** + * Create a command with a code and an int values + * @param command + * @param value + * @return + */ + private static byte[] createReq(byte[] command, int value) { + byte[] array = new byte[8]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(value, array, 4); + + return array; + } + + /** + * Creates the data array for a stat request. + * @param command the 4 byte command (ID_STAT, ID_RECV, ...) + * @param path The path of the remote file on which to execute the command + * @return the byte[] to send to the device through adb + */ + private static byte[] createFileReq(byte[] command, String path) { + byte[] pathContent = null; + try { + pathContent = path.getBytes(AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + return null; + } + + return createFileReq(command, pathContent); + } + + /** + * Creates the data array for a file request. This creates an array with a 4 byte command + the + * remote file name. + * @param command the 4 byte command (ID_STAT, ID_RECV, ...). + * @param path The path, as a byte array, of the remote file on which to + * execute the command. + * @return the byte[] to send to the device through adb + */ + private static byte[] createFileReq(byte[] command, byte[] path) { + byte[] array = new byte[8 + path.length]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(path.length, array, 4); + System.arraycopy(path, 0, array, 8, path.length); + + return array; + } + + private static byte[] createSendFileReq(byte[] command, byte[] path, int mode) { + // make the mode into a string + String modeStr = "," + (mode & 0777); // $NON-NLS-1S + byte[] modeContent = null; + try { + modeContent = modeStr.getBytes(AdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + return null; + } + + byte[] array = new byte[8 + path.length + modeContent.length]; + + System.arraycopy(command, 0, array, 0, 4); + ArrayHelper.swap32bitsToArray(path.length + modeContent.length, array, 4); + System.arraycopy(path, 0, array, 8, path.length); + System.arraycopy(modeContent, 0, array, 8 + path.length, modeContent.length); + + return array; + + + } + + /** + * Checks the result array starts with the provided code + * @param result The result array to check + * @param code The 4 byte code. + * @return true if the code matches. + */ + private static boolean checkResult(byte[] result, byte[] code) { + if (result[0] != code[0] || + result[1] != code[1] || + result[2] != code[2] || + result[3] != code[3]) { + return false; + } + + return true; + + } + + private static int getFileType(int mode) { + if ((mode & S_ISOCK) == S_ISOCK) { + return FileListingService.TYPE_SOCKET; + } + + if ((mode & S_IFLNK) == S_IFLNK) { + return FileListingService.TYPE_LINK; + } + + if ((mode & S_IFREG) == S_IFREG) { + return FileListingService.TYPE_FILE; + } + + if ((mode & S_IFBLK) == S_IFBLK) { + return FileListingService.TYPE_BLOCK; + } + + if ((mode & S_IFDIR) == S_IFDIR) { + return FileListingService.TYPE_DIRECTORY; + } + + if ((mode & S_IFCHR) == S_IFCHR) { + return FileListingService.TYPE_CHARACTER; + } + + if ((mode & S_IFIFO) == S_IFIFO) { + return FileListingService.TYPE_FIFO; + } + + return FileListingService.TYPE_OTHER; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java new file mode 100644 index 000000000..8f284f3d5 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2007 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.ddmlib; + +/** + * Holds a thread information. + */ +public final class ThreadInfo implements IStackTraceInfo { + private int mThreadId; + private String mThreadName; + private int mStatus; + private int mTid; + private int mUtime; + private int mStime; + private boolean mIsDaemon; + private StackTraceElement[] mTrace; + private long mTraceTime; + + // priority? + // total CPU used? + // method at top of stack? + + /** + * Construct with basic identification. + */ + ThreadInfo(int threadId, String threadName) { + mThreadId = threadId; + mThreadName = threadName; + + mStatus = -1; + //mTid = mUtime = mStime = 0; + //mIsDaemon = false; + } + + /** + * Set with the values we get from a THST chunk. + */ + void updateThread(int status, int tid, int utime, int stime, boolean isDaemon) { + + mStatus = status; + mTid = tid; + mUtime = utime; + mStime = stime; + mIsDaemon = isDaemon; + } + + /** + * Sets the stack call of the thread. + * @param trace stackcall information. + */ + void setStackCall(StackTraceElement[] trace) { + mTrace = trace; + mTraceTime = System.currentTimeMillis(); + } + + /** + * Returns the thread's ID. + */ + public int getThreadId() { + return mThreadId; + } + + /** + * Returns the thread's name. + */ + public String getThreadName() { + return mThreadName; + } + + void setThreadName(String name) { + mThreadName = name; + } + + /** + * Returns the system tid. + */ + public int getTid() { + return mTid; + } + + /** + * Returns the VM thread status. + */ + public int getStatus() { + return mStatus; + } + + /** + * Returns the cumulative user time. + */ + public int getUtime() { + return mUtime; + } + + /** + * Returns the cumulative system time. + */ + public int getStime() { + return mStime; + } + + /** + * Returns whether this is a daemon thread. + */ + public boolean isDaemon() { + return mIsDaemon; + } + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IStackTraceInfo#getStackTrace() + */ + public StackTraceElement[] getStackTrace() { + return mTrace; + } + + /** + * Returns the approximate time of the stacktrace data. + * @see #getStackTrace() + */ + public long getStackCallTime() { + return mTraceTime; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java new file mode 100644 index 000000000..ec9186c8e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java @@ -0,0 +1,461 @@ +/* + * 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.ddmlib.log; + +import com.android.ddmlib.log.LogReceiver.LogEntry; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents an event and its data. + */ +public class EventContainer { + + /** + * Comparison method for {@link EventContainer#testValue(int, Object, com.android.ddmlib.log.EventContainer.CompareMethod)} + * + */ + public enum CompareMethod { + EQUAL_TO("equals", "=="), + LESSER_THAN("less than or equals to", "<="), + LESSER_THAN_STRICT("less than", "<"), + GREATER_THAN("greater than or equals to", ">="), + GREATER_THAN_STRICT("greater than", ">"), + BIT_CHECK("bit check", "&"); + + private final String mName; + private final String mTestString; + + private CompareMethod(String name, String testString) { + mName = name; + mTestString = testString; + } + + /** + * Returns the display string. + */ + @Override + public String toString() { + return mName; + } + + /** + * Returns a short string representing the comparison. + */ + public String testString() { + return mTestString; + } + } + + + /** + * Type for event data. + */ + public static enum EventValueType { + UNKNOWN(0), + INT(1), + LONG(2), + STRING(3), + LIST(4), + TREE(5); + + private final static Pattern STORAGE_PATTERN = Pattern.compile("^(\\d+)@(.*)$"); //$NON-NLS-1$ + + private int mValue; + + /** + * Returns a {@link EventValueType} from an integer value, or null if no match + * was found. + * @param value the integer value. + */ + static EventValueType getEventValueType(int value) { + for (EventValueType type : values()) { + if (type.mValue == value) { + return type; + } + } + + return null; + } + + /** + * Returns a storage string for an {@link Object} of type supported by + * {@link EventValueType}. + *

+ * Strings created by this method can be reloaded with + * {@link #getObjectFromStorageString(String)}. + *

+ * NOTE: for now, only {@link #STRING}, {@link #INT}, and {@link #LONG} are supported. + * @param object the object to "convert" into a storage string. + * @return a string storing the object and its type or null if the type was not recognized. + */ + public static String getStorageString(Object object) { + if (object instanceof String) { + return STRING.mValue + "@" + (String)object; //$NON-NLS-1$ + } else if (object instanceof Integer) { + return INT.mValue + "@" + object.toString(); //$NON-NLS-1$ + } else if (object instanceof Long) { + return LONG.mValue + "@" + object.toString(); //$NON-NLS-1$ + } + + return null; + } + + /** + * Creates an {@link Object} from a storage string created with + * {@link #getStorageString(Object)}. + * @param value the storage string + * @return an {@link Object} or null if the string or type were not recognized. + */ + public static Object getObjectFromStorageString(String value) { + Matcher m = STORAGE_PATTERN.matcher(value); + if (m.matches()) { + try { + EventValueType type = getEventValueType(Integer.parseInt(m.group(1))); + + if (type == null) { + return null; + } + + switch (type) { + case STRING: + return m.group(2); + case INT: + return Integer.valueOf(m.group(2)); + case LONG: + return Long.valueOf(m.group(2)); + } + } catch (NumberFormatException nfe) { + return null; + } + } + + return null; + } + + + /** + * Returns the integer value of the enum. + */ + public int getValue() { + return mValue; + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + private EventValueType(int value) { + mValue = value; + } + } + + public int mTag; + public int pid; /* generating process's pid */ + public int tid; /* generating process's tid */ + public int sec; /* seconds since Epoch */ + public int nsec; /* nanoseconds */ + + private Object mData; + + /** + * Creates an {@link EventContainer} from a {@link LogEntry}. + * @param entry the LogEntry from which pid, tid, and time info is copied. + * @param tag the event tag value + * @param data the data of the EventContainer. + */ + EventContainer(LogEntry entry, int tag, Object data) { + getType(data); + mTag = tag; + mData = data; + + pid = entry.pid; + tid = entry.tid; + sec = entry.sec; + nsec = entry.nsec; + } + + /** + * Creates an {@link EventContainer} with raw data + */ + EventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) { + getType(data); + mTag = tag; + mData = data; + + this.pid = pid; + this.tid = tid; + this.sec = sec; + this.nsec = nsec; + } + + /** + * Returns the data as an int. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}. + * @see #getType() + */ + public final Integer getInt() throws InvalidTypeException { + if (getType(mData) == EventValueType.INT) { + return (Integer)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns the data as a long. + * @throws InvalidTypeException if the data type is not {@link EventValueType#LONG}. + * @see #getType() + */ + public final Long getLong() throws InvalidTypeException { + if (getType(mData) == EventValueType.LONG) { + return (Long)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns the data as a String. + * @throws InvalidTypeException if the data type is not {@link EventValueType#STRING}. + * @see #getType() + */ + public final String getString() throws InvalidTypeException { + if (getType(mData) == EventValueType.STRING) { + return (String)mData; + } + + throw new InvalidTypeException(); + } + + /** + * Returns a value by index. The return type is defined by its type. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + */ + public Object getValue(int valueIndex) { + return getValue(mData, valueIndex, true); + } + + /** + * Returns a value by index as a double. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}, + * {@link EventValueType#LONG}, {@link EventValueType#LIST}, or if the item in the + * list at index valueIndex is not of type {@link EventValueType#INT} or + * {@link EventValueType#LONG}. + * @see #getType() + */ + public double getValueAsDouble(int valueIndex) throws InvalidTypeException { + return getValueAsDouble(mData, valueIndex, true); + } + + /** + * Returns a value by index as a String. + * @param valueIndex the index of the value. If the data is not a list, this is ignored. + * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}, + * {@link EventValueType#LONG}, {@link EventValueType#STRING}, {@link EventValueType#LIST}, + * or if the item in the list at index valueIndex is not of type + * {@link EventValueType#INT}, {@link EventValueType#LONG}, or {@link EventValueType#STRING} + * @see #getType() + */ + public String getValueAsString(int valueIndex) throws InvalidTypeException { + return getValueAsString(mData, valueIndex, true); + } + + /** + * Returns the type of the data. + */ + public EventValueType getType() { + return getType(mData); + } + + /** + * Returns the type of an object. + */ + public final EventValueType getType(Object data) { + if (data instanceof Integer) { + return EventValueType.INT; + } else if (data instanceof Long) { + return EventValueType.LONG; + } else if (data instanceof String) { + return EventValueType.STRING; + } else if (data instanceof Object[]) { + // loop through the list to see if we have another list + Object[] objects = (Object[])data; + for (Object obj : objects) { + EventValueType type = getType(obj); + if (type == EventValueType.LIST || type == EventValueType.TREE) { + return EventValueType.TREE; + } + } + return EventValueType.LIST; + } + + return EventValueType.UNKNOWN; + } + + /** + * Checks that the index-th value of this event against a provided value. + * @param index the index of the value to test + * @param value the value to test against + * @param compareMethod the method of testing + * @return true if the test passed. + * @throws InvalidTypeException in case of type mismatch between the value to test and the value + * to test against, or if the compare method is incompatible with the type of the values. + * @see CompareMethod + */ + public boolean testValue(int index, Object value, + CompareMethod compareMethod) throws InvalidTypeException { + EventValueType type = getType(mData); + if (index > 0 && type != EventValueType.LIST) { + throw new InvalidTypeException(); + } + + Object data = mData; + if (type == EventValueType.LIST) { + data = ((Object[])mData)[index]; + } + + if (data.getClass().equals(data.getClass()) == false) { + throw new InvalidTypeException(); + } + + switch (compareMethod) { + case EQUAL_TO: + return data.equals(value); + case LESSER_THAN: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) <= 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) <= 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case LESSER_THAN_STRICT: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) < 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) < 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case GREATER_THAN: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) >= 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) >= 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case GREATER_THAN_STRICT: + if (data instanceof Integer) { + return (((Integer)data).compareTo((Integer)value) > 0); + } else if (data instanceof Long) { + return (((Long)data).compareTo((Long)value) > 0); + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + case BIT_CHECK: + if (data instanceof Integer) { + return (((Integer)data).intValue() & ((Integer)value).intValue()) != 0; + } else if (data instanceof Long) { + return (((Long)data).longValue() & ((Long)value).longValue()) != 0; + } + + // other types can't use this compare method. + throw new InvalidTypeException(); + default : + throw new InvalidTypeException(); + } + } + + private final Object getValue(Object data, int valueIndex, boolean recursive) { + EventValueType type = getType(data); + + switch (type) { + case INT: + case LONG: + case STRING: + return data; + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValue(list[valueIndex], valueIndex, false); + } + } + } + + return null; + } + + private final double getValueAsDouble(Object data, int valueIndex, boolean recursive) + throws InvalidTypeException { + EventValueType type = getType(data); + + switch (type) { + case INT: + return ((Integer)data).doubleValue(); + case LONG: + return ((Long)data).doubleValue(); + case STRING: + throw new InvalidTypeException(); + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValueAsDouble(list[valueIndex], valueIndex, false); + } + } + } + + throw new InvalidTypeException(); + } + + private final String getValueAsString(Object data, int valueIndex, boolean recursive) + throws InvalidTypeException { + EventValueType type = getType(data); + + switch (type) { + case INT: + return ((Integer)data).toString(); + case LONG: + return ((Long)data).toString(); + case STRING: + return (String)data; + case LIST: + if (recursive) { + Object[] list = (Object[]) data; + if (valueIndex >= 0 && valueIndex < list.length) { + return getValueAsString(list[valueIndex], valueIndex, false); + } + } else { + throw new InvalidTypeException( + "getValueAsString() doesn't support EventValueType.TREE"); + } + } + + throw new InvalidTypeException( + "getValueAsString() unsupported type:" + type); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java new file mode 100644 index 000000000..0b2ce6926 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java @@ -0,0 +1,577 @@ +/* + * 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.ddmlib.log; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.LogReceiver.LogEntry; +import com.android.ddmlib.utils.ArrayHelper; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser for the "event" log. + */ +public final class EventLogParser { + + /** Location of the tag map file on the device */ + private final static String EVENT_TAG_MAP_FILE = "/system/etc/event-log-tags"; //$NON-NLS-1$ + + /** + * Event log entry types. These must match up with the declarations in + * java/android/android/util/EventLog.java. + */ + private final static int EVENT_TYPE_INT = 0; + private final static int EVENT_TYPE_LONG = 1; + private final static int EVENT_TYPE_STRING = 2; + private final static int EVENT_TYPE_LIST = 3; + + private final static Pattern PATTERN_SIMPLE_TAG = Pattern.compile( + "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$ + private final static Pattern PATTERN_TAG_WITH_DESC = Pattern.compile( + "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$ + private final static Pattern PATTERN_DESCRIPTION = Pattern.compile( + "\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$ + + private final static Pattern TEXT_LOG_LINE = Pattern.compile( + "(\\d\\d)-(\\d\\d)\\s(\\d\\d):(\\d\\d):(\\d\\d).(\\d{3})\\s+I/([a-zA-Z0-9_]+)\\s*\\(\\s*(\\d+)\\):\\s+(.*)"); //$NON-NLS-1$ + + private final TreeMap mTagMap = new TreeMap(); + + private final TreeMap mValueDescriptionMap = + new TreeMap(); + + public EventLogParser() { + } + + /** + * Inits the parser for a specific Device. + *

+ * This methods reads the event-log-tags located on the device to find out + * what tags are being written to the event log and what their format is. + * @param device The device. + * @return true if success, false if failure or cancellation. + */ + public boolean init(IDevice device) { + // read the event tag map file on the device. + try { + device.executeShellCommand("cat " + EVENT_TAG_MAP_FILE, //$NON-NLS-1$ + new MultiLineReceiver() { + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + processTagLine(line); + } + } + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + return false; + } + + return true; + } + + /** + * Inits the parser with the content of a tag file. + * @param tagFileContent the lines of a tag file. + * @return true if success, false if failure. + */ + public boolean init(String[] tagFileContent) { + for (String line : tagFileContent) { + processTagLine(line); + } + return true; + } + + /** + * Inits the parser with a specified event-log-tags file. + * @param filePath + * @return true if success, false if failure. + */ + public boolean init(String filePath) { + try { + BufferedReader reader = new BufferedReader(new FileReader(filePath)); + + String line = null; + do { + line = reader.readLine(); + if (line != null) { + processTagLine(line); + } + } while (line != null); + + return true; + } catch (IOException e) { + return false; + } + } + + /** + * Processes a line from the event-log-tags file. + * @param line the line to process + */ + private void processTagLine(String line) { + // ignore empty lines and comment lines + if (line.length() > 0 && line.charAt(0) != '#') { + Matcher m = PATTERN_TAG_WITH_DESC.matcher(line); + if (m.matches()) { + try { + int value = Integer.parseInt(m.group(1)); + String name = m.group(2); + if (name != null && mTagMap.get(value) == null) { + mTagMap.put(value, name); + } + + // special case for the GC tag. We ignore what is in the file, + // and take what the custom GcEventContainer class tells us. + // This is due to the event encoding several values on 2 longs. + // @see GcEventContainer + if (value == GcEventContainer.GC_EVENT_TAG) { + mValueDescriptionMap.put(value, + GcEventContainer.getValueDescriptions()); + } else { + + String description = m.group(3); + if (description != null && description.length() > 0) { + EventValueDescription[] desc = + processDescription(description); + + if (desc != null) { + mValueDescriptionMap.put(value, desc); + } + } + } + } catch (NumberFormatException e) { + // failed to convert the number into a string. just ignore it. + } + } else { + m = PATTERN_SIMPLE_TAG.matcher(line); + if (m.matches()) { + int value = Integer.parseInt(m.group(1)); + String name = m.group(2); + if (name != null && mTagMap.get(value) == null) { + mTagMap.put(value, name); + } + } + } + } + } + + private EventValueDescription[] processDescription(String description) { + String[] descriptions = description.split("\\s*,\\s*"); //$NON-NLS-1$ + + ArrayList list = new ArrayList(); + + for (String desc : descriptions) { + Matcher m = PATTERN_DESCRIPTION.matcher(desc); + if (m.matches()) { + try { + String name = m.group(1); + + String typeString = m.group(2); + int typeValue = Integer.parseInt(typeString); + EventValueType eventValueType = EventValueType.getEventValueType(typeValue); + if (eventValueType == null) { + // just ignore this description if the value is not recognized. + // TODO: log the error. + } + + typeString = m.group(3); + if (typeString != null && typeString.length() > 0) { + //skip the | + typeString = typeString.substring(1); + + typeValue = Integer.parseInt(typeString); + ValueType valueType = ValueType.getValueType(typeValue); + + list.add(new EventValueDescription(name, eventValueType, valueType)); + } else { + list.add(new EventValueDescription(name, eventValueType)); + } + } catch (NumberFormatException nfe) { + // just ignore this description if one number is malformed. + // TODO: log the error. + } catch (InvalidValueTypeException e) { + // just ignore this description if data type and data unit don't match + // TODO: log the error. + } + } else { + Log.e("EventLogParser", //$NON-NLS-1$ + String.format("Can't parse %1$s", description)); //$NON-NLS-1$ + } + } + + if (list.size() == 0) { + return null; + } + + return list.toArray(new EventValueDescription[list.size()]); + + } + + public EventContainer parse(LogEntry entry) { + if (entry.len < 4) { + return null; + } + + int inOffset = 0; + + int tagValue = ArrayHelper.swap32bitFromArray(entry.data, inOffset); + inOffset += 4; + + String tag = mTagMap.get(tagValue); + if (tag == null) { + Log.e("EventLogParser", String.format("unknown tag number: %1$d", tagValue)); + } + + ArrayList list = new ArrayList(); + if (parseBinaryEvent(entry.data, inOffset, list) == -1) { + return null; + } + + Object data; + if (list.size() == 1) { + data = list.get(0); + } else{ + data = list.toArray(); + } + + EventContainer event = null; + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + event = new GcEventContainer(entry, tagValue, data); + } else { + event = new EventContainer(entry, tagValue, data); + } + + return event; + } + + public EventContainer parse(String textLogLine) { + // line will look like + // 04-29 23:16:16.691 I/dvm_gc_info( 427): + // where is either + // [value1,value2...] + // or + // value + if (textLogLine.length() == 0) { + return null; + } + + // parse the header first + Matcher m = TEXT_LOG_LINE.matcher(textLogLine); + if (m.matches()) { + try { + int month = Integer.parseInt(m.group(1)); + int day = Integer.parseInt(m.group(2)); + int hours = Integer.parseInt(m.group(3)); + int minutes = Integer.parseInt(m.group(4)); + int seconds = Integer.parseInt(m.group(5)); + int milliseconds = Integer.parseInt(m.group(6)); + + // convert into seconds since epoch and nano-seconds. + Calendar cal = Calendar.getInstance(); + cal.set(cal.get(Calendar.YEAR), month-1, day, hours, minutes, seconds); + int sec = (int)Math.floor(cal.getTimeInMillis()/1000); + int nsec = milliseconds * 1000000; + + String tag = m.group(7); + + // get the numerical tag value + int tagValue = -1; + Set> tagSet = mTagMap.entrySet(); + for (Entry entry : tagSet) { + if (tag.equals(entry.getValue())) { + tagValue = entry.getKey(); + break; + } + } + + if (tagValue == -1) { + return null; + } + + int pid = Integer.parseInt(m.group(8)); + + Object data = parseTextData(m.group(9), tagValue); + if (data == null) { + return null; + } + + // now we can allocate and return the EventContainer + EventContainer event = null; + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + event = new GcEventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data); + } else { + event = new EventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data); + } + + return event; + } catch (NumberFormatException e) { + return null; + } + } + + return null; + } + + public Map getTagMap() { + return mTagMap; + } + + public Map getEventInfoMap() { + return mValueDescriptionMap; + } + + /** + * Recursively convert binary log data to printable form. + * + * This needs to be recursive because you can have lists of lists. + * + * If we run out of room, we stop processing immediately. It's important + * for us to check for space on every output element to avoid producing + * garbled output. + * + * Returns the amount read on success, -1 on failure. + */ + private static int parseBinaryEvent(byte[] eventData, int dataOffset, ArrayList list) { + + if (eventData.length - dataOffset < 1) + return -1; + + int offset = dataOffset; + + int type = eventData[offset++]; + + //fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen); + + switch (type) { + case EVENT_TYPE_INT: { /* 32-bit signed int */ + int ival; + + if (eventData.length - offset < 4) + return -1; + ival = ArrayHelper.swap32bitFromArray(eventData, offset); + offset += 4; + + list.add(new Integer(ival)); + } + break; + case EVENT_TYPE_LONG: { /* 64-bit signed long */ + long lval; + + if (eventData.length - offset < 8) + return -1; + lval = ArrayHelper.swap64bitFromArray(eventData, offset); + offset += 8; + + list.add(new Long(lval)); + } + break; + case EVENT_TYPE_STRING: { /* UTF-8 chars, not NULL-terminated */ + int strLen; + + if (eventData.length - offset < 4) + return -1; + strLen = ArrayHelper.swap32bitFromArray(eventData, offset); + offset += 4; + + if (eventData.length - offset < strLen) + return -1; + + // get the string + try { + String str = new String(eventData, offset, strLen, "UTF-8"); //$NON-NLS-1$ + list.add(str); + } catch (UnsupportedEncodingException e) { + } + offset += strLen; + break; + } + case EVENT_TYPE_LIST: { /* N items, all different types */ + + if (eventData.length - offset < 1) + return -1; + + int count = eventData[offset++]; + + // make a new temp list + ArrayList subList = new ArrayList(); + for (int i = 0; i < count; i++) { + int result = parseBinaryEvent(eventData, offset, subList); + if (result == -1) { + return result; + } + + offset += result; + } + + list.add(subList.toArray()); + } + break; + default: + Log.e("EventLogParser", //$NON-NLS-1$ + String.format("Unknown binary event type %1$d", type)); //$NON-NLS-1$ + return -1; + } + + return offset - dataOffset; + } + + private Object parseTextData(String data, int tagValue) { + // first, get the description of what we're supposed to parse + EventValueDescription[] desc = mValueDescriptionMap.get(tagValue); + + if (desc == null) { + // TODO parse and create string values. + return null; + } + + if (desc.length == 1) { + return getObjectFromString(data, desc[0].getEventValueType()); + } else if (data.startsWith("[") && data.endsWith("]")) { + data = data.substring(1, data.length() - 1); + + // get each individual values as String + String[] values = data.split(","); + + if (tagValue == GcEventContainer.GC_EVENT_TAG) { + // special case for the GC event! + Object[] objects = new Object[2]; + + objects[0] = getObjectFromString(values[0], EventValueType.LONG); + objects[1] = getObjectFromString(values[1], EventValueType.LONG); + + return objects; + } else { + // must be the same number as the number of descriptors. + if (values.length != desc.length) { + return null; + } + + Object[] objects = new Object[values.length]; + + for (int i = 0 ; i < desc.length ; i++) { + Object obj = getObjectFromString(values[i], desc[i].getEventValueType()); + if (obj == null) { + return null; + } + objects[i] = obj; + } + + return objects; + } + } + + return null; + } + + + private Object getObjectFromString(String value, EventValueType type) { + try { + switch (type) { + case INT: + return Integer.valueOf(value); + case LONG: + return Long.valueOf(value); + case STRING: + return value; + } + } catch (NumberFormatException e) { + // do nothing, we'll return null. + } + + return null; + } + + /** + * Recreates the event-log-tags at the specified file path. + * @param filePath the file path to write the file. + * @throws IOException + */ + public void saveTags(String filePath) throws IOException { + File destFile = new File(filePath); + destFile.createNewFile(); + FileOutputStream fos = null; + + try { + + fos = new FileOutputStream(destFile); + + for (Integer key : mTagMap.keySet()) { + // get the tag name + String tagName = mTagMap.get(key); + + // get the value descriptions + EventValueDescription[] descriptors = mValueDescriptionMap.get(key); + + String line = null; + if (descriptors != null) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%1$d %2$s", key, tagName)); //$NON-NLS-1$ + boolean first = true; + for (EventValueDescription evd : descriptors) { + if (first) { + sb.append(" ("); //$NON-NLS-1$ + first = false; + } else { + sb.append(",("); //$NON-NLS-1$ + } + sb.append(evd.getName()); + sb.append("|"); //$NON-NLS-1$ + sb.append(evd.getEventValueType().getValue()); + sb.append("|"); //$NON-NLS-1$ + sb.append(evd.getValueType().getValue()); + sb.append("|)"); //$NON-NLS-1$ + } + sb.append("\n"); //$NON-NLS-1$ + + line = sb.toString(); + } else { + line = String.format("%1$d %2$s\n", key, tagName); //$NON-NLS-1$ + } + + byte[] buffer = line.getBytes(); + fos.write(buffer); + } + } finally { + if (fos != null) { + fos.close(); + } + } + } + + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java new file mode 100644 index 000000000..b68b4e8bb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java @@ -0,0 +1,214 @@ +/* + * 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.ddmlib.log; + +import com.android.ddmlib.log.EventContainer.EventValueType; + + +/** + * Describes an {@link EventContainer} value. + *

+ * This is a stand-alone object, not linked to a particular Event. It describes the value, by + * name, type ({@link EventValueType}), and (if needed) value unit ({@link ValueType}). + *

+ * The index of the value is not contained within this class, and is instead dependent on the + * index of this particular object in the array of {@link EventValueDescription} returned by + * {@link EventLogParser#getEventInfoMap()} when queried for a particular event tag. + * + */ +public final class EventValueDescription { + + /** + * Represents the type of a numerical value. This is used to display values of vastly different + * type/range in graphs. + */ + public static enum ValueType { + NOT_APPLICABLE(0), + OBJECTS(1), + BYTES(2), + MILLISECONDS(3), + ALLOCATIONS(4), + ID(5), + PERCENT(6); + + private int mValue; + + /** + * Checks that the {@link EventValueType} is compatible with the {@link ValueType}. + * @param type the {@link EventValueType} to check. + * @throws InvalidValueTypeException if the types are not compatible. + */ + public void checkType(EventValueType type) throws InvalidValueTypeException { + if ((type != EventValueType.INT && type != EventValueType.LONG) + && this != NOT_APPLICABLE) { + throw new InvalidValueTypeException( + String.format("%1$s doesn't support type %2$s", type, this)); + } + } + + /** + * Returns a {@link ValueType} from an integer value, or null if no match + * were found. + * @param value the integer value. + */ + public static ValueType getValueType(int value) { + for (ValueType type : values()) { + if (type.mValue == value) { + return type; + } + } + return null; + } + + /** + * Returns the integer value of the enum. + */ + public int getValue() { + return mValue; + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + private ValueType(int value) { + mValue = value; + } + } + + private String mName; + private EventValueType mEventValueType; + private ValueType mValueType; + + /** + * Builds a {@link EventValueDescription} with a name and a type. + *

+ * If the type is {@link EventValueType#INT} or {@link EventValueType#LONG}, the + * {@link #mValueType} is set to {@link ValueType#BYTES} by default. It set to + * {@link ValueType#NOT_APPLICABLE} for all other {@link EventValueType} values. + * @param name + * @param type + */ + EventValueDescription(String name, EventValueType type) { + mName = name; + mEventValueType = type; + if (mEventValueType == EventValueType.INT || mEventValueType == EventValueType.LONG) { + mValueType = ValueType.BYTES; + } else { + mValueType = ValueType.NOT_APPLICABLE; + } + } + + /** + * Builds a {@link EventValueDescription} with a name and a type, and a {@link ValueType}. + *

+ * @param name + * @param type + * @param valueType + * @throws InvalidValueTypeException if type and valuetype are not compatible. + * + */ + EventValueDescription(String name, EventValueType type, ValueType valueType) + throws InvalidValueTypeException { + mName = name; + mEventValueType = type; + mValueType = valueType; + mValueType.checkType(mEventValueType); + } + + /** + * @return the Name. + */ + public String getName() { + return mName; + } + + /** + * @return the {@link EventValueType}. + */ + public EventValueType getEventValueType() { + return mEventValueType; + } + + /** + * @return the {@link ValueType}. + */ + public ValueType getValueType() { + return mValueType; + } + + @Override + public String toString() { + if (mValueType != ValueType.NOT_APPLICABLE) { + return String.format("%1$s (%2$s, %3$s)", mName, mEventValueType.toString(), + mValueType.toString()); + } + + return String.format("%1$s (%2$s)", mName, mEventValueType.toString()); + } + + /** + * Checks if the value is of the proper type for this receiver. + * @param value the value to check. + * @return true if the value is of the proper type for this receiver. + */ + public boolean checkForType(Object value) { + switch (mEventValueType) { + case INT: + return value instanceof Integer; + case LONG: + return value instanceof Long; + case STRING: + return value instanceof String; + case LIST: + return value instanceof Object[]; + } + + return false; + } + + /** + * Returns an object of a valid type (based on the value returned by + * {@link #getEventValueType()}) from a String value. + *

+ * IMPORTANT {@link EventValueType#LIST} and {@link EventValueType#TREE} are not + * supported. + * @param value the value of the object expressed as a string. + * @return an object or null if the conversion could not be done. + */ + public Object getObjectFromString(String value) { + switch (mEventValueType) { + case INT: + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + case LONG: + try { + return Long.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + case STRING: + return value; + } + + return null; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java new file mode 100644 index 000000000..7bae20249 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java @@ -0,0 +1,347 @@ +/* + * 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.ddmlib.log; + +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.LogReceiver.LogEntry; + +/** + * Custom Event Container for the Gc event since this event doesn't simply output data in + * int or long format, but encodes several values on 4 longs. + *

+ * The array of {@link EventValueDescription}s parsed from the "event-log-tags" file must + * be ignored, and instead, the array returned from {@link #getValueDescriptions()} must be used. + */ +final class GcEventContainer extends EventContainer { + + public final static int GC_EVENT_TAG = 20001; + + private String processId; + private long gcTime; + private long bytesFreed; + private long objectsFreed; + private long actualSize; + private long allowedSize; + private long softLimit; + private long objectsAllocated; + private long bytesAllocated; + private long zActualSize; + private long zAllowedSize; + private long zObjectsAllocated; + private long zBytesAllocated; + private long dlmallocFootprint; + private long mallinfoTotalAllocatedSpace; + private long externalLimit; + private long externalBytesAllocated; + + GcEventContainer(LogEntry entry, int tag, Object data) { + super(entry, tag, data); + init(data); + } + + GcEventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) { + super(tag, pid, tid, sec, nsec, data); + init(data); + } + + /** + * @param data + */ + private void init(Object data) { + if (data instanceof Object[]) { + Object[] values = (Object[])data; + for (int i = 0; i < values.length; i++) { + if (values[i] instanceof Long) { + parseDvmHeapInfo((Long)values[i], i); + } + } + } + } + + @Override + public EventValueType getType() { + return EventValueType.LIST; + } + + @Override + public boolean testValue(int index, Object value, CompareMethod compareMethod) + throws InvalidTypeException { + // do a quick easy check on the type. + if (index == 0) { + if ((value instanceof String) == false) { + throw new InvalidTypeException(); + } + } else if ((value instanceof Long) == false) { + throw new InvalidTypeException(); + } + + switch (compareMethod) { + case EQUAL_TO: + if (index == 0) { + return processId.equals(value); + } else { + return getValueAsLong(index) == ((Long)value).longValue(); + } + case LESSER_THAN: + return getValueAsLong(index) <= ((Long)value).longValue(); + case LESSER_THAN_STRICT: + return getValueAsLong(index) < ((Long)value).longValue(); + case GREATER_THAN: + return getValueAsLong(index) >= ((Long)value).longValue(); + case GREATER_THAN_STRICT: + return getValueAsLong(index) > ((Long)value).longValue(); + case BIT_CHECK: + return (getValueAsLong(index) & ((Long)value).longValue()) != 0; + } + + throw new ArrayIndexOutOfBoundsException(); + } + + @Override + public Object getValue(int valueIndex) { + if (valueIndex == 0) { + return processId; + } + + try { + return new Long(getValueAsLong(valueIndex)); + } catch (InvalidTypeException e) { + // this would only happened if valueIndex was 0, which we test above. + } + + return null; + } + + @Override + public double getValueAsDouble(int valueIndex) throws InvalidTypeException { + return (double)getValueAsLong(valueIndex); + } + + @Override + public String getValueAsString(int valueIndex) { + switch (valueIndex) { + case 0: + return processId; + default: + try { + return Long.toString(getValueAsLong(valueIndex)); + } catch (InvalidTypeException e) { + // we shouldn't stop there since we test, in this method first. + } + } + + throw new ArrayIndexOutOfBoundsException(); + } + + /** + * Returns a custom array of {@link EventValueDescription} since the actual content of this + * event (list of (long, long) does not match the values encoded into those longs. + */ + static EventValueDescription[] getValueDescriptions() { + try { + return new EventValueDescription[] { + new EventValueDescription("Process Name", EventValueType.STRING), + new EventValueDescription("GC Time", EventValueType.LONG, + ValueType.MILLISECONDS), + new EventValueDescription("Freed Objects", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Freed Bytes", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Soft Limit", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Actual Size (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allowed Size (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allocated Objects (aggregate)", + EventValueType.LONG, ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes (aggregate)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Actual Size", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Allowed Size", EventValueType.LONG, ValueType.BYTES), + new EventValueDescription("Allocated Objects", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Actual Size (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allowed Size (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Allocated Objects (zygote)", EventValueType.LONG, + ValueType.OBJECTS), + new EventValueDescription("Allocated Bytes (zygote)", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("External Allocation Limit", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("External Bytes Allocated", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("dlmalloc Footprint", EventValueType.LONG, + ValueType.BYTES), + new EventValueDescription("Malloc Info: Total Allocated Space", + EventValueType.LONG, ValueType.BYTES), + }; + } catch (InvalidValueTypeException e) { + // this shouldn't happen since we control manual the EventValueType and the ValueType + // values. For development purpose, we assert if this happens. + assert false; + } + + // this shouldn't happen, but the compiler complains otherwise. + return null; + } + + private void parseDvmHeapInfo(long data, int index) { + switch (index) { + case 0: + // [63 ] Must be zero + // [62-24] ASCII process identifier + // [23-12] GC time in ms + // [11- 0] Bytes freed + + gcTime = float12ToInt((int)((data >> 12) & 0xFFFL)); + bytesFreed = float12ToInt((int)(data & 0xFFFL)); + + // convert the long into an array, in the proper order so that we can convert the + // first 5 char into a string. + byte[] dataArray = new byte[8]; + put64bitsToArray(data, dataArray, 0); + + // get the name from the string + processId = new String(dataArray, 0, 5); + break; + case 1: + // [63-62] 10 + // [61-60] Reserved; must be zero + // [59-48] Objects freed + // [47-36] Actual size (current footprint) + // [35-24] Allowed size (current hard max) + // [23-12] Objects allocated + // [11- 0] Bytes allocated + objectsFreed = float12ToInt((int)((data >> 48) & 0xFFFL)); + actualSize = float12ToInt((int)((data >> 36) & 0xFFFL)); + allowedSize = float12ToInt((int)((data >> 24) & 0xFFFL)); + objectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL)); + bytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + case 2: + // [63-62] 11 + // [61-60] Reserved; must be zero + // [59-48] Soft limit (current soft max) + // [47-36] Actual size (current footprint) + // [35-24] Allowed size (current hard max) + // [23-12] Objects allocated + // [11- 0] Bytes allocated + softLimit = float12ToInt((int)((data >> 48) & 0xFFFL)); + zActualSize = float12ToInt((int)((data >> 36) & 0xFFFL)); + zAllowedSize = float12ToInt((int)((data >> 24) & 0xFFFL)); + zObjectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL)); + zBytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + case 3: + // [63-48] Reserved; must be zero + // [47-36] dlmallocFootprint + // [35-24] mallinfo: total allocated space + // [23-12] External byte limit + // [11- 0] External bytes allocated + dlmallocFootprint = float12ToInt((int)((data >> 36) & 0xFFFL)); + mallinfoTotalAllocatedSpace = float12ToInt((int)((data >> 24) & 0xFFFL)); + externalLimit = float12ToInt((int)((data >> 12) & 0xFFFL)); + externalBytesAllocated = float12ToInt((int)(data & 0xFFFL)); + break; + default: + break; + } + } + + /** + * Converts a 12 bit float representation into an unsigned int (returned as a long) + * @param f12 + */ + private static long float12ToInt(int f12) { + return (f12 & 0x1FF) << ((f12 >>> 9) * 4); + } + + /** + * puts an unsigned value in an array. + * @param value The value to put. + * @param dest the destination array + * @param offset the offset in the array where to put the value. + * Array length must be at least offset + 8 + */ + private static void put64bitsToArray(long value, byte[] dest, int offset) { + dest[offset + 7] = (byte)(value & 0x00000000000000FFL); + dest[offset + 6] = (byte)((value & 0x000000000000FF00L) >> 8); + dest[offset + 5] = (byte)((value & 0x0000000000FF0000L) >> 16); + dest[offset + 4] = (byte)((value & 0x00000000FF000000L) >> 24); + dest[offset + 3] = (byte)((value & 0x000000FF00000000L) >> 32); + dest[offset + 2] = (byte)((value & 0x0000FF0000000000L) >> 40); + dest[offset + 1] = (byte)((value & 0x00FF000000000000L) >> 48); + dest[offset + 0] = (byte)((value & 0xFF00000000000000L) >> 56); + } + + /** + * Returns the long value of the valueIndex-th value. + * @param valueIndex the index of the value. + * @throws InvalidTypeException if index is 0 as it is a string value. + */ + private final long getValueAsLong(int valueIndex) throws InvalidTypeException { + switch (valueIndex) { + case 0: + throw new InvalidTypeException(); + case 1: + return gcTime; + case 2: + return objectsFreed; + case 3: + return bytesFreed; + case 4: + return softLimit; + case 5: + return actualSize; + case 6: + return allowedSize; + case 7: + return objectsAllocated; + case 8: + return bytesAllocated; + case 9: + return actualSize - zActualSize; + case 10: + return allowedSize - zAllowedSize; + case 11: + return objectsAllocated - zObjectsAllocated; + case 12: + return bytesAllocated - zBytesAllocated; + case 13: + return zActualSize; + case 14: + return zAllowedSize; + case 15: + return zObjectsAllocated; + case 16: + return zBytesAllocated; + case 17: + return externalLimit; + case 18: + return externalBytesAllocated; + case 19: + return dlmallocFootprint; + case 20: + return mallinfoTotalAllocatedSpace; + } + + throw new ArrayIndexOutOfBoundsException(); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java new file mode 100644 index 000000000..016f8aa14 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java @@ -0,0 +1,74 @@ +/* + * 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.ddmlib.log; + +import java.io.Serializable; + +/** + * Exception thrown when accessing an {@link EventContainer} value with the wrong type. + */ +public final class InvalidTypeException extends Exception { + + /** + * Needed by {@link Serializable}. + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with the default detail message. + * @see java.lang.Exception + */ + public InvalidTypeException() { + super("Invalid Type"); + } + + /** + * Constructs a new exception with the specified detail message. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @see java.lang.Exception + */ + public InvalidTypeException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause and a detail message of + * (cause==null ? null : cause.toString()) (which typically contains + * the class and detail message of cause). + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidTypeException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java new file mode 100644 index 000000000..a3050c8c7 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java @@ -0,0 +1,78 @@ +/* + * 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.ddmlib.log; + +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventValueDescription.ValueType; + +import java.io.Serializable; + +/** + * Exception thrown when associating an {@link EventValueType} with an incompatible + * {@link ValueType}. + */ +public final class InvalidValueTypeException extends Exception { + + /** + * Needed by {@link Serializable}. + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with the default detail message. + * @see java.lang.Exception + */ + public InvalidValueTypeException() { + super("Invalid Type"); + } + + /** + * Constructs a new exception with the specified detail message. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @see java.lang.Exception + */ + public InvalidValueTypeException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause and a detail message of + * (cause==null ? null : cause.toString()) (which typically contains + * the class and detail message of cause). + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidValueTypeException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * @param message the detail message. The detail message is saved for later retrieval + * by the {@link Throwable#getMessage()} method. + * @param cause the cause (which is saved for later retrieval by the + * {@link Throwable#getCause()} method). (A null value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @see java.lang.Exception + */ + public InvalidValueTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java new file mode 100644 index 000000000..b49f0256e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java @@ -0,0 +1,247 @@ +/* + * 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.ddmlib.log; + + +import com.android.ddmlib.utils.ArrayHelper; + +import java.security.InvalidParameterException; + +/** + * Receiver able to provide low level parsing for device-side log services. + */ +public final class LogReceiver { + + private final static int ENTRY_HEADER_SIZE = 20; // 2*2 + 4*4; see LogEntry. + + /** + * Represents a log entry and its raw data. + */ + public final static class LogEntry { + /* + * See //device/include/utils/logger.h + */ + /** 16bit unsigned: length of the payload. */ + public int len; /* This is normally followed by a 16 bit padding */ + /** pid of the process that generated this {@link LogEntry} */ + public int pid; + /** tid of the process that generated this {@link LogEntry} */ + public int tid; + /** Seconds since epoch. */ + public int sec; + /** nanoseconds. */ + public int nsec; + /** The entry's raw data. */ + public byte[] data; + }; + + /** + * Classes which implement this interface provide a method that deals + * with {@link LogEntry} objects coming from log service through a {@link LogReceiver}. + *

This interface provides two methods. + *

    + *
  • {@link #newEntry(com.android.ddmlib.log.LogReceiver.LogEntry)} provides a + * first level of parsing, extracting {@link LogEntry} objects out of the log service output.
  • + *
  • {@link #newData(byte[], int, int)} provides a way to receive the raw information + * coming directly from the log service.
  • + *
+ */ + public interface ILogListener { + /** + * Sent when a new {@link LogEntry} has been parsed by the {@link LogReceiver}. + * @param entry the new log entry. + */ + public void newEntry(LogEntry entry); + + /** + * Sent when new raw data is coming from the log service. + * @param data the raw data buffer. + * @param offset the offset into the buffer signaling the beginning of the new data. + * @param length the length of the new data. + */ + public void newData(byte[] data, int offset, int length); + } + + /** Current {@link LogEntry} being read, before sending it to the listener. */ + private LogEntry mCurrentEntry; + + /** Temp buffer to store partial entry headers. */ + private byte[] mEntryHeaderBuffer = new byte[ENTRY_HEADER_SIZE]; + /** Offset in the partial header buffer */ + private int mEntryHeaderOffset = 0; + /** Offset in the partial entry data */ + private int mEntryDataOffset = 0; + + /** Listener waiting for receive fully read {@link LogEntry} objects */ + private ILogListener mListener; + + private boolean mIsCancelled = false; + + /** + * Creates a {@link LogReceiver} with an {@link ILogListener}. + *

+ * The {@link ILogListener} will receive new log entries as they are parsed, in the form + * of {@link LogEntry} objects. + * @param listener the listener to receive new log entries. + */ + public LogReceiver(ILogListener listener) { + mListener = listener; + } + + + /** + * Parses new data coming from the log service. + * @param data the data buffer + * @param offset the offset into the buffer signaling the beginning of the new data. + * @param length the length of the new data. + */ + public void parseNewData(byte[] data, int offset, int length) { + // notify the listener of new raw data + if (mListener != null) { + mListener.newData(data, offset, length); + } + + // loop while there is still data to be read and the receiver has not be cancelled. + while (length > 0 && mIsCancelled == false) { + // first check if we have no current entry. + if (mCurrentEntry == null) { + if (mEntryHeaderOffset + length < ENTRY_HEADER_SIZE) { + // if we don't have enough data to finish the header, save + // the data we have and return + System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, length); + mEntryHeaderOffset += length; + return; + } else { + // we have enough to fill the header, let's do it. + // did we store some part at the beginning of the header? + if (mEntryHeaderOffset != 0) { + // copy the rest of the entry header into the header buffer + int size = ENTRY_HEADER_SIZE - mEntryHeaderOffset; + System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, + size); + + // create the entry from the header buffer + mCurrentEntry = createEntry(mEntryHeaderBuffer, 0); + + // since we used the whole entry header buffer, we reset the offset + mEntryHeaderOffset = 0; + + // adjust current offset and remaining length to the beginning + // of the entry data + offset += size; + length -= size; + } else { + // create the entry directly from the data array + mCurrentEntry = createEntry(data, offset); + + // adjust current offset and remaining length to the beginning + // of the entry data + offset += ENTRY_HEADER_SIZE; + length -= ENTRY_HEADER_SIZE; + } + } + } + + // at this point, we have an entry, and offset/length have been updated to skip + // the entry header. + + // if we have enough data for this entry or more, we'll need to end this entry + if (length >= mCurrentEntry.len - mEntryDataOffset) { + // compute and save the size of the data that we have to read for this entry, + // based on how much we may already have read. + int dataSize = mCurrentEntry.len - mEntryDataOffset; + + // we only read what we need, and put it in the entry buffer. + System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, dataSize); + + // notify the listener of a new entry + if (mListener != null) { + mListener.newEntry(mCurrentEntry); + } + + // reset some flags: we have read 0 data of the current entry. + // and we have no current entry being read. + mEntryDataOffset = 0; + mCurrentEntry = null; + + // and update the data buffer info to the end of the current entry / start + // of the next one. + offset += dataSize; + length -= dataSize; + } else { + // we don't have enough data to fill this entry, so we store what we have + // in the entry itself. + System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, length); + + // save the amount read for the data. + mEntryDataOffset += length; + return; + } + } + } + + /** + * Returns whether this receiver is canceling the remote service. + */ + public boolean isCancelled() { + return mIsCancelled; + } + + /** + * Cancels the current remote service. + */ + public void cancel() { + mIsCancelled = true; + } + + /** + * Creates a {@link LogEntry} from the array of bytes. This expects the data buffer size + * to be at least offset + {@link #ENTRY_HEADER_SIZE}. + * @param data the data buffer the entry is read from. + * @param offset the offset of the first byte from the buffer representing the entry. + * @return a new {@link LogEntry} or null if some error happened. + */ + private LogEntry createEntry(byte[] data, int offset) { + if (data.length < offset + ENTRY_HEADER_SIZE) { + throw new InvalidParameterException( + "Buffer not big enough to hold full LoggerEntry header"); + } + + // create the new entry and fill it. + LogEntry entry = new LogEntry(); + entry.len = ArrayHelper.swapU16bitFromArray(data, offset); + + // we've read only 16 bits, but since there's also a 16 bit padding, + // we can skip right over both. + offset += 4; + + entry.pid = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.tid = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.sec = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.nsec = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + + // allocate the data + entry.data = new byte[entry.len]; + + return entry; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java new file mode 100644 index 000000000..b61a69861 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java @@ -0,0 +1,85 @@ +/* + * 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.ddmlib.testrunner; + +/** + * Receives event notifications during instrumentation test runs. + * Patterned after {@link junit.runner.TestRunListener}. + */ +public interface ITestRunListener { + + /** + * Types of test failures. + */ + enum TestFailure { + /** Test failed due to unanticipated uncaught exception. */ + ERROR, + /** Test failed due to a false assertion. */ + FAILURE + } + + /** + * Reports the start of a test run. + * + * @param testCount total number of tests in test run + */ + public void testRunStarted(int testCount); + + /** + * Reports end of test run. + * + * @param elapsedTime device reported elapsed time, in milliseconds + */ + public void testRunEnded(long elapsedTime); + + /** + * Reports test run stopped before completion. + * + * @param elapsedTime device reported elapsed time, in milliseconds + */ + public void testRunStopped(long elapsedTime); + + /** + * Reports the start of an individual test case. + * + * @param test identifies the test + */ + public void testStarted(TestIdentifier test); + + /** + * Reports the execution end of an individual test case. + * If {@link #testFailed} was not invoked, this test passed. + * + * @param test identifies the test + */ + public void testEnded(TestIdentifier test); + + /** + * Reports the failure of a individual test case. + * Will be called between testStarted and testEnded. + * + * @param status failure type + * @param test identifies the test + * @param trace stack trace of failure + */ + public void testFailed(TestFailure status, TestIdentifier test, String trace); + + /** + * Reports test run failed to execute due to a fatal error. + */ + public void testRunFailed(String errorMessage); +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java new file mode 100755 index 000000000..1a0b21fdb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java @@ -0,0 +1,409 @@ +/* + * 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.ddmlib.testrunner; + +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; + +/** + * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a + * ITestRunListener of the results. + * + *

Expects the following output: + * + *

If fatal error occurred when attempted to run the tests: + *

+ * INSTRUMENTATION_STATUS: Error=error Message
+ * INSTRUMENTATION_FAILED: 
+ * 
+ *

or + *

+ * INSTRUMENTATION_RESULT: shortMsg=error Message
+ * 
+ * + *

Otherwise, expect a series of test results, each one containing a set of status key/value + * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test + * run, expects that the elapsed test time in seconds will be displayed + * + *

For example: + *

+ * INSTRUMENTATION_STATUS_CODE: 1
+ * INSTRUMENTATION_STATUS: class=com.foo.FooTest
+ * INSTRUMENTATION_STATUS: test=testFoo
+ * INSTRUMENTATION_STATUS: numtests=2
+ * INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312
+ *    com.foo.X
+ * INSTRUMENTATION_STATUS_CODE: -2   
+ * ... 
+ * 
+ * Time: X
+ * 
+ *

Note that the "value" portion of the key-value pair may wrap over several text lines + */ +public class InstrumentationResultParser extends MultiLineReceiver { + + /** Relevant test status keys. */ + private static class StatusKeys { + private static final String TEST = "test"; + private static final String CLASS = "class"; + private static final String STACK = "stack"; + private static final String NUMTESTS = "numtests"; + private static final String ERROR = "Error"; + private static final String SHORTMSG = "shortMsg"; + } + + /** Test result status codes. */ + private static class StatusCodes { + private static final int FAILURE = -2; + private static final int START = 1; + private static final int ERROR = -1; + private static final int OK = 0; + } + + /** Prefixes used to identify output. */ + private static class Prefixes { + private static final String STATUS = "INSTRUMENTATION_STATUS: "; + private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: "; + private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: "; + private static final String CODE = "INSTRUMENTATION_CODE: "; + private static final String RESULT = "INSTRUMENTATION_RESULT: "; + private static final String TIME_REPORT = "Time: "; + } + + private final ITestRunListener mTestListener; + + /** + * Test result data + */ + private static class TestResult { + private Integer mCode = null; + private String mTestName = null; + private String mTestClass = null; + private String mStackTrace = null; + private Integer mNumTests = null; + + /** Returns true if all expected values have been parsed */ + boolean isComplete() { + return mCode != null && mTestName != null && mTestClass != null; + } + + /** Provides a more user readable string for TestResult, if possible */ + @Override + public String toString() { + StringBuilder output = new StringBuilder(); + if (mTestClass != null ) { + output.append(mTestClass); + output.append('#'); + } + if (mTestName != null) { + output.append(mTestName); + } + if (output.length() > 0) { + return output.toString(); + } + return "unknown result"; + } + } + + /** Stores the status values for the test result currently being parsed */ + private TestResult mCurrentTestResult = null; + + /** Stores the current "key" portion of the status key-value being parsed. */ + private String mCurrentKey = null; + + /** Stores the current "value" portion of the status key-value being parsed. */ + private StringBuilder mCurrentValue = null; + + /** True if start of test has already been reported to listener. */ + private boolean mTestStartReported = false; + + /** The elapsed time of the test run, in milliseconds. */ + private long mTestTime = 0; + + /** True if current test run has been canceled by user. */ + private boolean mIsCancelled = false; + + private static final String LOG_TAG = "InstrumentationResultParser"; + + /** + * Creates the InstrumentationResultParser. + * + * @param listener informed of test results as the tests are executing + */ + public InstrumentationResultParser(ITestRunListener listener) { + mTestListener = listener; + } + + /** + * Processes the instrumentation test output from shell. + * + * @see MultiLineReceiver#processNewLines + */ + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + parse(line); + // in verbose mode, dump all adb output to log + Log.v(LOG_TAG, line); + } + } + + /** + * Parse an individual output line. Expects a line that is one of: + *

    + *
  • + * The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE), + * and thus there is a new key=value pair to parse, and the previous key-value pair is + * finished. + *
  • + *
  • + * A continuation of the previous status (the "value" portion of the key has wrapped + * to the next line). + *
  • + *
  • A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED)
  • + *
  • A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT)
  • + *
+ * + * @param line Text output line + */ + private void parse(String line) { + if (line.startsWith(Prefixes.STATUS_CODE)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + parseStatusCode(line); + } else if (line.startsWith(Prefixes.STATUS)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + parseKey(line, Prefixes.STATUS.length()); + } else if (line.startsWith(Prefixes.RESULT)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + parseKey(line, Prefixes.RESULT.length()); + } else if (line.startsWith(Prefixes.STATUS_FAILED) || + line.startsWith(Prefixes.CODE)) { + // Previous status key-value has been collected. Store it. + submitCurrentKeyValue(); + // just ignore the remaining data on this line + } else if (line.startsWith(Prefixes.TIME_REPORT)) { + parseTime(line, Prefixes.TIME_REPORT.length()); + } else { + if (mCurrentValue != null) { + // this is a value that has wrapped to next line. + mCurrentValue.append("\r\n"); + mCurrentValue.append(line); + } else { + Log.w(LOG_TAG, "unrecognized line " + line); + } + } + } + + /** + * Stores the currently parsed key-value pair into mCurrentTestInfo. + */ + private void submitCurrentKeyValue() { + if (mCurrentKey != null && mCurrentValue != null) { + TestResult testInfo = getCurrentTestInfo(); + String statusValue = mCurrentValue.toString(); + + if (mCurrentKey.equals(StatusKeys.CLASS)) { + testInfo.mTestClass = statusValue.trim(); + } else if (mCurrentKey.equals(StatusKeys.TEST)) { + testInfo.mTestName = statusValue.trim(); + } else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) { + try { + testInfo.mNumTests = Integer.parseInt(statusValue); + } catch (NumberFormatException e) { + Log.e(LOG_TAG, "Unexpected integer number of tests, received " + statusValue); + } + } else if (mCurrentKey.equals(StatusKeys.ERROR) || + mCurrentKey.equals(StatusKeys.SHORTMSG)) { + // test run must have failed + handleTestRunFailed(statusValue); + } else if (mCurrentKey.equals(StatusKeys.STACK)) { + testInfo.mStackTrace = statusValue; + } + + mCurrentKey = null; + mCurrentValue = null; + } + } + + private TestResult getCurrentTestInfo() { + if (mCurrentTestResult == null) { + mCurrentTestResult = new TestResult(); + } + return mCurrentTestResult; + } + + private void clearCurrentTestInfo() { + mCurrentTestResult = null; + } + + /** + * Parses the key from the current line. + * Expects format of "key=value". + * + * @param line full line of text to parse + * @param keyStartPos the starting position of the key in the given line + */ + private void parseKey(String line, int keyStartPos) { + int endKeyPos = line.indexOf('=', keyStartPos); + if (endKeyPos != -1) { + mCurrentKey = line.substring(keyStartPos, endKeyPos).trim(); + parseValue(line, endKeyPos + 1); + } + } + + /** + * Parses the start of a key=value pair. + * + * @param line - full line of text to parse + * @param valueStartPos - the starting position of the value in the given line + */ + private void parseValue(String line, int valueStartPos) { + mCurrentValue = new StringBuilder(); + mCurrentValue.append(line.substring(valueStartPos)); + } + + /** + * Parses out a status code result. + */ + private void parseStatusCode(String line) { + String value = line.substring(Prefixes.STATUS_CODE.length()).trim(); + TestResult testInfo = getCurrentTestInfo(); + try { + testInfo.mCode = Integer.parseInt(value); + } catch (NumberFormatException e) { + Log.e(LOG_TAG, "Expected integer status code, received: " + value); + } + + // this means we're done with current test result bundle + reportResult(testInfo); + clearCurrentTestInfo(); + } + + /** + * Returns true if test run canceled. + * + * @see IShellOutputReceiver#isCancelled() + */ + public boolean isCancelled() { + return mIsCancelled; + } + + /** + * Requests cancellation of test run. + */ + public void cancel() { + mIsCancelled = true; + } + + /** + * Reports a test result to the test run listener. Must be called when a individual test + * result has been fully parsed. + * + * @param statusMap key-value status pairs of test result + */ + private void reportResult(TestResult testInfo) { + if (!testInfo.isComplete()) { + Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString()); + return; + } + reportTestRunStarted(testInfo); + TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName); + + switch (testInfo.mCode) { + case StatusCodes.START: + mTestListener.testStarted(testId); + break; + case StatusCodes.FAILURE: + mTestListener.testFailed(ITestRunListener.TestFailure.FAILURE, testId, + getTrace(testInfo)); + mTestListener.testEnded(testId); + break; + case StatusCodes.ERROR: + mTestListener.testFailed(ITestRunListener.TestFailure.ERROR, testId, + getTrace(testInfo)); + mTestListener.testEnded(testId); + break; + case StatusCodes.OK: + mTestListener.testEnded(testId); + break; + default: + Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode); + mTestListener.testEnded(testId); + break; + } + + } + + /** + * Reports the start of a test run, and the total test count, if it has not been previously + * reported. + * + * @param testInfo current test status values + */ + private void reportTestRunStarted(TestResult testInfo) { + // if start test run not reported yet + if (!mTestStartReported && testInfo.mNumTests != null) { + mTestListener.testRunStarted(testInfo.mNumTests); + mTestStartReported = true; + } + } + + /** + * Returns the stack trace of the current failed test, from the provided testInfo. + */ + private String getTrace(TestResult testInfo) { + if (testInfo.mStackTrace != null) { + return testInfo.mStackTrace; + } else { + Log.e(LOG_TAG, "Could not find stack trace for failed test "); + return new Throwable("Unknown failure").toString(); + } + } + + /** + * Parses out and store the elapsed time. + */ + private void parseTime(String line, int startPos) { + String timeString = line.substring(startPos); + try { + float timeSeconds = Float.parseFloat(timeString); + mTestTime = (long) (timeSeconds * 1000); + } catch (NumberFormatException e) { + Log.e(LOG_TAG, "Unexpected time format " + timeString); + } + } + + /** + * Process a instrumentation run failure + */ + private void handleTestRunFailed(String errorMsg) { + mTestListener.testRunFailed(errorMsg == null ? "Unknown error" : errorMsg); + } + + /** + * Called by parent when adb session is complete. + */ + @Override + public void done() { + super.done(); + mTestListener.testRunEnded(mTestTime); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java new file mode 100644 index 000000000..9dd1d1640 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java @@ -0,0 +1,251 @@ +/* + * 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.ddmlib.testrunner; + + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; + +import java.io.IOException; +import java.util.Hashtable; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Runs a Android test command remotely and reports results. + */ +public class RemoteAndroidTestRunner { + + private final String mPackageName; + private final String mRunnerName; + private IDevice mRemoteDevice; + /** map of name-value instrumentation argument pairs */ + private Map mArgMap; + private InstrumentationResultParser mParser; + + private static final String LOG_TAG = "RemoteAndroidTest"; + private static final String DEFAULT_RUNNER_NAME = "android.test.InstrumentationTestRunner"; + + private static final char CLASS_SEPARATOR = ','; + private static final char METHOD_SEPARATOR = '#'; + private static final char RUNNER_SEPARATOR = '/'; + + // defined instrumentation argument names + private static final String CLASS_ARG_NAME = "class"; + private static final String LOG_ARG_NAME = "log"; + private static final String DEBUG_ARG_NAME = "debug"; + private static final String COVERAGE_ARG_NAME = "coverage"; + private static final String PACKAGE_ARG_NAME = "package"; + + /** + * Creates a remote Android test runner. + * + * @param packageName the Android application package that contains the tests to run + * @param runnerName the instrumentation test runner to execute. If null, will use default + * runner + * @param remoteDevice the Android device to execute tests on + */ + public RemoteAndroidTestRunner(String packageName, + String runnerName, + IDevice remoteDevice) { + + mPackageName = packageName; + mRunnerName = runnerName; + mRemoteDevice = remoteDevice; + mArgMap = new Hashtable(); + } + + /** + * Alternate constructor. Uses default instrumentation runner. + * + * @param packageName the Android application package that contains the tests to run + * @param remoteDevice the Android device to execute tests on + */ + public RemoteAndroidTestRunner(String packageName, + IDevice remoteDevice) { + this(packageName, null, remoteDevice); + } + + /** + * Returns the application package name. + */ + public String getPackageName() { + return mPackageName; + } + + /** + * Returns the runnerName. + */ + public String getRunnerName() { + if (mRunnerName == null) { + return DEFAULT_RUNNER_NAME; + } + return mRunnerName; + } + + /** + * Returns the complete instrumentation component path. + */ + private String getRunnerPath() { + return getPackageName() + RUNNER_SEPARATOR + getRunnerName(); + } + + /** + * Sets to run only tests in this class + * Must be called before 'run'. + * + * @param className fully qualified class name (eg x.y.z) + */ + public void setClassName(String className) { + addInstrumentationArg(CLASS_ARG_NAME, className); + } + + /** + * Sets to run only tests in the provided classes + * Must be called before 'run'. + *

+ * If providing more than one class, requires a InstrumentationTestRunner that supports + * the multiple class argument syntax. + * + * @param classNames array of fully qualified class names (eg x.y.z) + */ + public void setClassNames(String[] classNames) { + StringBuilder classArgBuilder = new StringBuilder(); + + for (int i = 0; i < classNames.length; i++) { + if (i != 0) { + classArgBuilder.append(CLASS_SEPARATOR); + } + classArgBuilder.append(classNames[i]); + } + setClassName(classArgBuilder.toString()); + } + + /** + * Sets to run only specified test method + * Must be called before 'run'. + * + * @param className fully qualified class name (eg x.y.z) + * @param testName method name + */ + public void setMethodName(String className, String testName) { + setClassName(className + METHOD_SEPARATOR + testName); + } + + /** + * Sets to run all tests in specified package + * Must be called before 'run'. + * + * @param packageName fully qualified package name (eg x.y.z) + */ + public void setTestPackageName(String packageName) { + addInstrumentationArg(PACKAGE_ARG_NAME, packageName); + } + + /** + * Adds a argument to include in instrumentation command. + *

+ * Must be called before 'run'. If an argument with given name has already been provided, it's + * value will be overridden. + * + * @param name the name of the instrumentation bundle argument + * @param value the value of the argument + */ + public void addInstrumentationArg(String name, String value) { + if (name == null || value == null) { + throw new IllegalArgumentException("name or value arguments cannot be null"); + } + mArgMap.put(name, value); + } + + /** + * Adds a boolean argument to include in instrumentation command. + *

+ * @see RemoteAndroidTestRunner#addInstrumentationArg + * + * @param name the name of the instrumentation bundle argument + * @param value the value of the argument + */ + public void addBooleanArg(String name, boolean value) { + addInstrumentationArg(name, Boolean.toString(value)); + } + + /** + * Sets this test run to log only mode - skips test execution. + */ + public void setLogOnly(boolean logOnly) { + addBooleanArg(LOG_ARG_NAME, logOnly); + } + + /** + * Sets this debug mode of this test run. If true, the Android test runner will wait for a + * debugger to attach before proceeding with test execution. + */ + public void setDebug(boolean debug) { + addBooleanArg(DEBUG_ARG_NAME, debug); + } + + /** + * Sets this code coverage mode of this test run. + */ + public void setCoverage(boolean coverage) { + addBooleanArg(COVERAGE_ARG_NAME, coverage); + } + + /** + * Execute this test run. + * + * @param listener listens for test results + */ + public void run(ITestRunListener listener) { + final String runCaseCommandStr = String.format("am instrument -w -r %s %s", + getArgsCommand(), getRunnerPath()); + Log.d(LOG_TAG, runCaseCommandStr); + mParser = new InstrumentationResultParser(listener); + + try { + mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser); + } catch (IOException e) { + Log.e(LOG_TAG, e); + listener.testRunFailed(e.toString()); + } + } + + /** + * Requests cancellation of this test run. + */ + public void cancel() { + if (mParser != null) { + mParser.cancel(); + } + } + + /** + * Returns the full instrumentation command line syntax for the provided instrumentation + * arguments. + * Returns an empty string if no arguments were specified. + */ + private String getArgsCommand() { + StringBuilder commandBuilder = new StringBuilder(); + for (Entry argPair : mArgMap.entrySet()) { + final String argCmd = String.format(" -e %s %s", argPair.getKey(), + argPair.getValue()); + commandBuilder.append(argCmd); + } + return commandBuilder.toString(); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java new file mode 100644 index 000000000..4d3b1080b --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java @@ -0,0 +1,76 @@ +/* + * 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.ddmlib.testrunner; + +/** + * Identifies a parsed instrumentation test + */ +public class TestIdentifier { + + private final String mClassName; + private final String mTestName; + + /** + * Creates a test identifier + * + * @param className fully qualified class name of the test. Cannot be null. + * @param testName name of the test. Cannot be null. + */ + public TestIdentifier(String className, String testName) { + if (className == null || testName == null) { + throw new IllegalArgumentException("className and testName must " + + "be non-null"); + } + mClassName = className; + mTestName = testName; + } + + /** + * Returns the fully qualified class name of the test + */ + public String getClassName() { + return mClassName; + } + + /** + * Returns the name of the test + */ + public String getTestName() { + return mTestName; + } + + /** + * Tests equality by comparing class and method name + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof TestIdentifier)) { + return false; + } + TestIdentifier otherTest = (TestIdentifier)other; + return getClassName().equals(otherTest.getClassName()) && + getTestName().equals(otherTest.getTestName()); + } + + /** + * Generates hashCode based on class and method name. + */ + @Override + public int hashCode() { + return getClassName().hashCode() * 31 + getTestName().hashCode(); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java new file mode 100644 index 000000000..8167e5dd9 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2007 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.ddmlib.utils; + +/** + * Utility class providing array to int/long conversion for data received from devices through adb. + */ +public final class ArrayHelper { + + /** + * Swaps an unsigned value around, and puts the result in an array that can be sent to a device. + * @param value The value to swap. + * @param dest the destination array + * @param offset the offset in the array where to put the swapped value. + * Array length must be at least offset + 4 + */ + public static void swap32bitsToArray(int value, byte[] dest, int offset) { + dest[offset] = (byte)(value & 0x000000FF); + dest[offset + 1] = (byte)((value & 0x0000FF00) >> 8); + dest[offset + 2] = (byte)((value & 0x00FF0000) >> 16); + dest[offset + 3] = (byte)((value & 0xFF000000) >> 24); + } + + /** + * Reads a signed 32 bit integer from an array coming from a device. + * @param value the array containing the int + * @param offset the offset in the array at which the int starts + * @return the integer read from the array + */ + public static int swap32bitFromArray(byte[] value, int offset) { + int v = 0; + v |= ((int)value[offset]) & 0x000000FF; + v |= (((int)value[offset + 1]) & 0x000000FF) << 8; + v |= (((int)value[offset + 2]) & 0x000000FF) << 16; + v |= (((int)value[offset + 3]) & 0x000000FF) << 24; + + return v; + } + + /** + * Reads an unsigned 16 bit integer from an array coming from a device, + * and returns it as an 'int' + * @param value the array containing the 16 bit int (2 byte). + * @param offset the offset in the array at which the int starts + * Array length must be at least offset + 2 + * @return the integer read from the array. + */ + public static int swapU16bitFromArray(byte[] value, int offset) { + int v = 0; + v |= ((int)value[offset]) & 0x000000FF; + v |= (((int)value[offset + 1]) & 0x000000FF) << 8; + + return v; + } + + /** + * Reads a signed 64 bit integer from an array coming from a device. + * @param value the array containing the int + * @param offset the offset in the array at which the int starts + * Array length must be at least offset + 8 + * @return the integer read from the array + */ + public static long swap64bitFromArray(byte[] value, int offset) { + long v = 0; + v |= ((long)value[offset]) & 0x00000000000000FFL; + v |= (((long)value[offset + 1]) & 0x00000000000000FFL) << 8; + v |= (((long)value[offset + 2]) & 0x00000000000000FFL) << 16; + v |= (((long)value[offset + 3]) & 0x00000000000000FFL) << 24; + v |= (((long)value[offset + 4]) & 0x00000000000000FFL) << 32; + v |= (((long)value[offset + 5]) & 0x00000000000000FFL) << 40; + v |= (((long)value[offset + 6]) & 0x00000000000000FFL) << 48; + v |= (((long)value[offset + 7]) & 0x00000000000000FFL) << 56; + + return v; + } +} diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java new file mode 100644 index 000000000..7742dd655 --- /dev/null +++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java @@ -0,0 +1,288 @@ +/* + * 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.ddmlib.testrunner; + +import junit.framework.TestCase; + + +/** + * Tests InstrumentationResultParser. + */ +public class InstrumentationResultParserTest extends TestCase { + + private InstrumentationResultParser mParser; + private VerifyingTestResult mTestResult; + + // static dummy test names to use for validation + private static final String CLASS_NAME = "com.test.FooTest"; + private static final String TEST_NAME = "testFoo"; + private static final String STACK_TRACE = "java.lang.AssertionFailedException"; + + /** + * @param name - test name + */ + public InstrumentationResultParserTest(String name) { + super(name); + } + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + mTestResult = new VerifyingTestResult(); + mParser = new InstrumentationResultParser(mTestResult); + } + + /** + * Tests that the test run started and test start events is sent on first + * bundle received. + */ + public void testTestStarted() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + assertEquals(0, mTestResult.mNumTestsRun); + } + + /** + * Tests that a single successful test execution. + */ + public void testTestSuccess() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + addCommonStatus(output); + addSuccessCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + assertEquals(1, mTestResult.mNumTestsRun); + assertEquals(null, mTestResult.mTestStatus); + } + + /** + * Test basic parsing of failed test case. + */ + public void testTestFailed() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + addCommonStatus(output); + addStackTrace(output); + addFailureCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + + assertEquals(1, mTestResult.mNumTestsRun); + assertEquals(ITestRunListener.TestFailure.FAILURE, mTestResult.mTestStatus); + assertEquals(STACK_TRACE, mTestResult.mTrace); + } + + /** + * Test basic parsing and conversion of time from output. + */ + public void testTimeParsing() { + final String timeString = "Time: 4.9"; + injectTestString(timeString); + assertEquals(4900, mTestResult.mTestTime); + } + + /** + * Test basic parsing of a test run failure. + */ + public void testRunFailed() { + StringBuilder output = new StringBuilder(); + final String errorMessage = "Unable to find instrumentation info"; + addStatusKey(output, "Error", errorMessage); + addStatusCode(output, "-1"); + output.append("INSTRUMENTATION_FAILED: com.dummy/android.test.InstrumentationTestRunner"); + addLineBreak(output); + + injectTestString(output.toString()); + + assertEquals(errorMessage, mTestResult.mRunFailedMessage); + } + + /** + * Test parsing of a test run failure, where an instrumentation component failed to load + * Parsing input takes the from of INSTRUMENTATION_RESULT: fff + */ + public void testRunFailedResult() { + StringBuilder output = new StringBuilder(); + final String errorMessage = "Unable to instantiate instrumentation"; + output.append("INSTRUMENTATION_RESULT: shortMsg="); + output.append(errorMessage); + addLineBreak(output); + output.append("INSTRUMENTATION_CODE: 0"); + addLineBreak(output); + + injectTestString(output.toString()); + + assertEquals(errorMessage, mTestResult.mRunFailedMessage); + } + + /** + * Builds a common test result using TEST_NAME and TEST_CLASS. + */ + private StringBuilder buildCommonResult() { + StringBuilder output = new StringBuilder(); + // add test start bundle + addCommonStatus(output); + addStatusCode(output, "1"); + // add end test bundle, without status + addCommonStatus(output); + return output; + } + + /** + * Adds common status results to the provided output. + */ + private void addCommonStatus(StringBuilder output) { + addStatusKey(output, "stream", "\r\n" + CLASS_NAME); + addStatusKey(output, "test", TEST_NAME); + addStatusKey(output, "class", CLASS_NAME); + addStatusKey(output, "current", "1"); + addStatusKey(output, "numtests", "1"); + addStatusKey(output, "id", "InstrumentationTestRunner"); + } + + /** + * Adds a stack trace status bundle to output. + */ + private void addStackTrace(StringBuilder output) { + addStatusKey(output, "stack", STACK_TRACE); + + } + + /** + * Helper method to add a status key-value bundle. + */ + private void addStatusKey(StringBuilder outputBuilder, String key, + String value) { + outputBuilder.append("INSTRUMENTATION_STATUS: "); + outputBuilder.append(key); + outputBuilder.append('='); + outputBuilder.append(value); + addLineBreak(outputBuilder); + } + + /** + * Append line break characters to output + */ + private void addLineBreak(StringBuilder outputBuilder) { + outputBuilder.append("\r\n"); + } + + private void addStartCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "1"); + } + + private void addSuccessCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "0"); + } + + private void addFailureCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "-2"); + } + + private void addStatusCode(StringBuilder outputBuilder, String value) { + outputBuilder.append("INSTRUMENTATION_STATUS_CODE: "); + outputBuilder.append(value); + addLineBreak(outputBuilder); + } + + /** + * inject a test string into the result parser. + * + * @param result + */ + private void injectTestString(String result) { + byte[] data = result.getBytes(); + mParser.addOutput(data, 0, data.length); + mParser.flush(); + } + + private void assertCommonAttributes() { + assertEquals(CLASS_NAME, mTestResult.mSuiteName); + assertEquals(1, mTestResult.mTestCount); + assertEquals(TEST_NAME, mTestResult.mTestName); + } + + /** + * A specialized test listener that stores a single test events. + */ + private class VerifyingTestResult implements ITestRunListener { + + String mSuiteName; + int mTestCount; + int mNumTestsRun; + String mTestName; + long mTestTime; + TestFailure mTestStatus; + String mTrace; + boolean mStopped; + /** stores the error message provided to testRunFailed */ + String mRunFailedMessage; + + VerifyingTestResult() { + mNumTestsRun = 0; + mTestStatus = null; + mStopped = false; + mRunFailedMessage = null; + } + + public void testEnded(TestIdentifier test) { + mNumTestsRun++; + assertEquals("Unexpected class name", mSuiteName, test.getClassName()); + assertEquals("Unexpected test ended", mTestName, test.getTestName()); + + } + + public void testFailed(TestFailure status, TestIdentifier test, String trace) { + mTestStatus = status; + mTrace = trace; + assertEquals("Unexpected class name", mSuiteName, test.getClassName()); + assertEquals("Unexpected test ended", mTestName, test.getTestName()); + } + + public void testRunEnded(long elapsedTime) { + mTestTime = elapsedTime; + + } + + public void testRunStarted(int testCount) { + mTestCount = testCount; + } + + public void testRunStopped(long elapsedTime) { + mTestTime = elapsedTime; + mStopped = true; + } + + public void testStarted(TestIdentifier test) { + mSuiteName = test.getClassName(); + mTestName = test.getTestName(); + } + + public void testRunFailed(String errorMessage) { + mRunFailedMessage = errorMessage; + } + } +} diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java new file mode 100644 index 000000000..29ec9fe6d --- /dev/null +++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java @@ -0,0 +1,282 @@ +/* + * 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.ddmlib.testrunner; + +import com.android.ddmlib.Client; +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.log.LogReceiver; + +import java.io.IOException; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * Tests RemoteAndroidTestRunner. + */ +public class RemoteAndroidTestRunnerTest extends TestCase { + + private RemoteAndroidTestRunner mRunner; + private MockDevice mMockDevice; + + private static final String TEST_PACKAGE = "com.test"; + private static final String TEST_RUNNER = "com.test.InstrumentationTestRunner"; + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + mMockDevice = new MockDevice(); + mRunner = new RemoteAndroidTestRunner(TEST_PACKAGE, TEST_RUNNER, mMockDevice); + } + + /** + * Test the basic case building of the instrumentation runner command with no arguments. + */ + public void testRun() { + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r %s/%s", TEST_PACKAGE, TEST_RUNNER), + mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with log set. + */ + public void testRunWithLog() { + mRunner.setLogOnly(true); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r -e log true %s/%s", TEST_PACKAGE, + TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with method set. + */ + public void testRunWithMethod() { + final String className = "FooTest"; + final String testName = "fooTest"; + mRunner.setMethodName(className, testName); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r -e class %s#%s %s/%s", className, + testName, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with test package set. + */ + public void testRunWithPackage() { + final String packageName = "foo.test"; + mRunner.setTestPackageName(packageName); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r -e package %s %s/%s", packageName, + TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with extra argument added. + */ + public void testRunWithAddInstrumentationArg() { + final String extraArgName = "blah"; + final String extraArgValue = "blahValue"; + mRunner.addInstrumentationArg(extraArgName, extraArgValue); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r -e %s %s %s/%s", extraArgName, + extraArgValue, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + + /** + * Assert two strings are equal ignoring whitespace. + */ + private void assertStringsEquals(String str1, String str2) { + String strippedStr1 = str1.replaceAll(" ", ""); + String strippedStr2 = str2.replaceAll(" ", ""); + assertEquals(strippedStr1, strippedStr2); + } + + /** + * A dummy device that does nothing except store the provided executed shell command for + * later retrieval. + */ + private static class MockDevice implements IDevice { + + private String mLastShellCommand; + + /** + * Stores the provided command for later retrieval from getLastShellCommand. + */ + public void executeShellCommand(String command, + IShellOutputReceiver receiver) throws IOException { + mLastShellCommand = command; + } + + /** + * Get the last command provided to executeShellCommand. + */ + public String getLastShellCommand() { + return mLastShellCommand; + } + + public boolean createForward(int localPort, int remotePort) { + throw new UnsupportedOperationException(); + } + + public Client getClient(String applicationName) { + throw new UnsupportedOperationException(); + } + + public String getClientName(int pid) { + throw new UnsupportedOperationException(); + } + + public Client[] getClients() { + throw new UnsupportedOperationException(); + } + + public FileListingService getFileListingService() { + throw new UnsupportedOperationException(); + } + + public Map getProperties() { + throw new UnsupportedOperationException(); + } + + public String getProperty(String name) { + throw new UnsupportedOperationException(); + } + + public int getPropertyCount() { + throw new UnsupportedOperationException(); + } + + public RawImage getScreenshot() throws IOException { + throw new UnsupportedOperationException(); + } + + public String getSerialNumber() { + throw new UnsupportedOperationException(); + } + + public DeviceState getState() { + throw new UnsupportedOperationException(); + } + + public SyncService getSyncService() { + throw new UnsupportedOperationException(); + } + + public boolean hasClients() { + throw new UnsupportedOperationException(); + } + + public boolean isBootLoader() { + throw new UnsupportedOperationException(); + } + + public boolean isEmulator() { + throw new UnsupportedOperationException(); + } + + public boolean isOffline() { + throw new UnsupportedOperationException(); + } + + public boolean isOnline() { + throw new UnsupportedOperationException(); + } + + public boolean removeForward(int localPort, int remotePort) { + throw new UnsupportedOperationException(); + } + + public void runEventLogService(LogReceiver receiver) throws IOException { + throw new UnsupportedOperationException(); + } + + public void runLogService(String logname, LogReceiver receiver) throws IOException { + throw new UnsupportedOperationException(); + } + + public String getAvdName() { + return ""; + } + + public String installPackage(String packageFilePath, boolean reinstall) + throws IOException { + throw new UnsupportedOperationException(); + } + + public String uninstallPackage(String packageName) throws IOException { + throw new UnsupportedOperationException(); + } + + public String installRemotePackage(String remoteFilePath, + boolean reinstall) throws IOException { + throw new UnsupportedOperationException(); + } + + public void removeRemotePackage(String remoteFilePath) + throws IOException { + throw new UnsupportedOperationException(); + } + + public String syncPackageToDevice(String localFilePath) + throws IOException { + throw new UnsupportedOperationException(); + } + + } + + /** + * An empty implementation of ITestRunListener. + */ + private static class EmptyListener implements ITestRunListener { + + public void testEnded(TestIdentifier test) { + // ignore + } + + public void testFailed(TestFailure status, TestIdentifier test, String trace) { + // ignore + } + + public void testRunEnded(long elapsedTime) { + // ignore + } + + public void testRunFailed(String errorMessage) { + // ignore + } + + public void testRunStarted(int testCount) { + // ignore + } + + public void testRunStopped(long elapsedTime) { + // ignore + } + + public void testStarted(TestIdentifier test) { + // ignore + } + } +} diff --git a/ddms/libs/ddmuilib/.classpath b/ddms/libs/ddmuilib/.classpath new file mode 100644 index 000000000..2cd368c29 --- /dev/null +++ b/ddms/libs/ddmuilib/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ddms/libs/ddmuilib/.project b/ddms/libs/ddmuilib/.project new file mode 100644 index 000000000..29cb2f2d3 --- /dev/null +++ b/ddms/libs/ddmuilib/.project @@ -0,0 +1,17 @@ + + + ddmuilib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/ddms/libs/ddmuilib/Android.mk b/ddms/libs/ddmuilib/Android.mk new file mode 100644 index 000000000..7059e5e5d --- /dev/null +++ b/ddms/libs/ddmuilib/Android.mk @@ -0,0 +1,4 @@ +# Copyright 2007 The Android Open Source Project +# +DDMUILIB_LOCAL_DIR := $(call my-dir) +include $(DDMUILIB_LOCAL_DIR)/src/Android.mk diff --git a/ddms/libs/ddmuilib/README b/ddms/libs/ddmuilib/README new file mode 100644 index 000000000..d66b84ade --- /dev/null +++ b/ddms/libs/ddmuilib/README @@ -0,0 +1,11 @@ +Using the Eclipse projects for ddmuilib. + +ddmuilib requires SWT to compile. + +SWT is available in the depot under prebuild//swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar +available at prebuild//swt. \ No newline at end of file diff --git a/ddms/libs/ddmuilib/src/Android.mk b/ddms/libs/ddmuilib/src/Android.mk new file mode 100644 index 000000000..68ceac1c9 --- /dev/null +++ b/ddms/libs/ddmuilib/src/Android.mk @@ -0,0 +1,22 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAVA_RESOURCE_DIRS := resources + +LOCAL_JAVA_LIBRARIES := \ + ddmlib \ + swt \ + org.eclipse.jface_3.4.2.M20090107-0800 \ + org.eclipse.equinox.common_3.4.0.v20080421-2006 \ + org.eclipse.core.commands_3.4.0.I20080509-2000 \ + jcommon-1.0.12 \ + jfreechart-1.0.9 \ + jfreechart-1.0.9-swt + +LOCAL_MODULE := ddmuilib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java new file mode 100644 index 000000000..47e093333 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Addr2Line.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.*; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.HashMap; + +/** + * Represents an addr2line process to get filename/method information from a + * memory address.
+ * Each process can only handle one library, which should be provided when + * creating a new process.
+ *
+ * The processes take some time to load as they need to parse the library files. + * For this reason, processes cannot be manually started. Instead the class + * keeps an internal list of processes and one asks for a process for a specific + * library, using getProcess(String library).

+ * Internally, the processes are started in pipe mode to be able to query them + * with multiple addresses. + */ +public class Addr2Line { + + /** + * Loaded processes list. This is also used as a locking object for any + * methods dealing with starting/stopping/creating processes/querying for + * method. + */ + private static final HashMap sProcessCache = + new HashMap(); + + /** + * byte array representing a carriage return. Used to push addresses in the + * process pipes. + */ + private static final byte[] sCrLf = { + '\n' + }; + + /** Path to the library */ + private String mLibrary; + + /** the command line process */ + private Process mProcess; + + /** buffer to read the result of the command line process from */ + private BufferedReader mResultReader; + + /** + * output stream to provide new addresses to decode to the command line + * process + */ + private BufferedOutputStream mAddressWriter; + + /** + * Returns the instance of a Addr2Line process for the specified library. + *
The library should be in a format that makes
+ * $ANDROID_PRODUCT_OUT + "/symbols" + library a valid file. + * + * @param library the library in which to look for addresses. + * @return a new Addr2Line object representing a started process, ready to + * be queried for addresses. If any error happened when launching a + * new process, null will be returned. + */ + public static Addr2Line getProcess(final String library) { + // synchronize around the hashmap object + if (library != null) { + synchronized (sProcessCache) { + // look for an existing process + Addr2Line process = sProcessCache.get(library); + + // if we don't find one, we create it + if (process == null) { + process = new Addr2Line(library); + + // then we start it + boolean status = process.start(); + + if (status) { + // if starting the process worked, then we add it to the + // list. + sProcessCache.put(library, process); + } else { + // otherwise we just drop the object, to return null + process = null; + } + } + // return the process + return process; + } + } + return null; + } + + /** + * Construct the object with a library name. + *
The library should be in a format that makes
+ * $ANDROID_PRODUCT_OUT + "/symbols" + library a valid file. + * + * @param library the library in which to look for address. + */ + private Addr2Line(final String library) { + mLibrary = library; + } + + /** + * Starts the command line process. + * + * @return true if the process was started, false if it failed to start, or + * if there was any other errors. + */ + private boolean start() { + // because this is only called from getProcess() we know we don't need + // to synchronize this code. + + // get the output directory. + String symbols = System.getenv("ANDROID_SYMBOLS"); + if (symbols == null) { + symbols = DdmUiPreferences.getSymbolDirectory(); + } + + // build the command line + String[] command = new String[5]; + command[0] = DdmUiPreferences.getAddr2Line(); + command[1] = "-C"; + command[2] = "-f"; + command[3] = "-e"; + command[4] = symbols + mLibrary.replaceAll("libc\\.so", "libc_debug\\.so"); + + try { + // attempt to start the process + mProcess = Runtime.getRuntime().exec(command); + + if (mProcess != null) { + // get the result reader + InputStreamReader is = new InputStreamReader(mProcess + .getInputStream()); + mResultReader = new BufferedReader(is); + + // get the outstream to write the addresses + mAddressWriter = new BufferedOutputStream(mProcess + .getOutputStream()); + + // check our streams are here + if (mResultReader == null || mAddressWriter == null) { + // not here? stop the process and return false; + mProcess.destroy(); + mProcess = null; + return false; + } + + // return a success + return true; + } + + } catch (IOException e) { + // log the error + String msg = String.format( + "Error while trying to start %1$s process for library %2$s", + DdmUiPreferences.getAddr2Line(), mLibrary); + Log.e("ddm-Addr2Line", msg); + + // drop the process just in case + if (mProcess != null) { + mProcess.destroy(); + mProcess = null; + } + } + + // we can be here either cause the allocation of mProcess failed, or we + // caught an exception + return false; + } + + /** + * Stops the command line process. + */ + public void stop() { + synchronized (sProcessCache) { + if (mProcess != null) { + // remove the process from the list + sProcessCache.remove(mLibrary); + + // then stops the process + mProcess.destroy(); + + // set the reference to null. + // this allows to make sure another thread calling getAddress() + // will not query a stopped thread + mProcess = null; + } + } + } + + /** + * Stops all current running processes. + */ + public static void stopAll() { + // because of concurrent access (and our use of HashMap.values()), we + // can't rely on the synchronized inside stop(). We need to put one + // around the whole loop. + synchronized (sProcessCache) { + // just a basic loop on all the values in the hashmap and call to + // stop(); + Collection col = sProcessCache.values(); + for (Addr2Line a2l : col) { + a2l.stop(); + } + } + } + + /** + * Looks up an address and returns method name, source file name, and line + * number. + * + * @param addr the address to look up + * @return a BacktraceInfo object containing the method/filename/linenumber + * or null if the process we stopped before the query could be + * processed, or if an IO exception happened. + */ + public NativeStackCallInfo getAddress(long addr) { + // even though we don't access the hashmap object, we need to + // synchronized on it to prevent + // another thread from stopping the process we're going to query. + synchronized (sProcessCache) { + // check the process is still alive/allocated + if (mProcess != null) { + // prepare to the write the address to the output buffer. + + // first, conversion to a string containing the hex value. + String tmp = Long.toString(addr, 16); + + try { + // write the address to the buffer + mAddressWriter.write(tmp.getBytes()); + + // add CR-LF + mAddressWriter.write(sCrLf); + + // flush it all. + mAddressWriter.flush(); + + // read the result. We need to read 2 lines + String method = mResultReader.readLine(); + String source = mResultReader.readLine(); + + // make the backtrace object and return it + if (method != null && source != null) { + return new NativeStackCallInfo(mLibrary, method, source); + } + } catch (IOException e) { + // log the error + Log.e("ddms", + "Error while trying to get information for addr: " + + tmp + " in library: " + mLibrary); + // we'll return null later + } + } + } + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java new file mode 100644 index 000000000..11c0e19c3 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/AllocationPanel.java @@ -0,0 +1,486 @@ +/* + * 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; + +import com.android.ddmlib.AllocationInfo; +import com.android.ddmlib.Client; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.ClientData.AllocationTrackingStatus; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Table; + +/** + * Base class for our information panels. + */ +public class AllocationPanel extends TablePanel { + + private final static String PREFS_ALLOC_COL_SIZE = "allocPanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_CLASS = "allocPanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_THREAD = "allocPanel.Col2"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_TRACE_CLASS = "allocPanel.Col3"; //$NON-NLS-1$ + private final static String PREFS_ALLOC_COL_TRACE_METHOD = "allocPanel.Col4"; //$NON-NLS-1$ + + private final static String PREFS_ALLOC_SASH = "allocPanel.sash"; //$NON-NLS-1$ + + private static final String PREFS_STACK_COL_CLASS = "allocPanel.stack.col0"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_METHOD = "allocPanel.stack.col1"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_FILE = "allocPanel.stack.col2"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_LINE = "allocPanel.stack.col3"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_NATIVE = "allocPanel.stack.col4"; //$NON-NLS-1$ + + private Composite mAllocationBase; + private Table mAllocationTable; + private TableViewer mAllocationViewer; + + private StackTracePanel mStackTracePanel; + private Table mStackTraceTable; + private Button mEnableButton; + private Button mRequestButton; + + /** + * Content Provider to display the allocations of a client. + * Expected input is a {@link Client} object, elements used in the table are of type + * {@link AllocationInfo}. + */ + private static class AllocationContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Client) { + AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations(); + if (allocs != null) { + return allocs; + } + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + /** + * A Label Provider to use with {@link AllocationContentProvider}. It expects the elements to be + * of type {@link AllocationInfo}. + */ + private static class AllocationLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof AllocationInfo) { + AllocationInfo alloc = (AllocationInfo)element; + switch (columnIndex) { + case 0: + return Integer.toString(alloc.getSize()); + case 1: + return alloc.getAllocatedClass(); + case 2: + return Short.toString(alloc.getThreadId()); + case 3: + StackTraceElement[] traces = alloc.getStackTrace(); + if (traces.length > 0) { + return traces[0].getClassName(); + } + break; + case 4: + traces = alloc.getStackTrace(); + if (traces.length > 0) { + return traces[0].getMethodName(); + } + break; + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + final IPreferenceStore store = DdmUiPreferences.getStore(); + + // base composite for selected client with enabled thread update. + mAllocationBase = new Composite(parent, SWT.NONE); + mAllocationBase.setLayout(new FormLayout()); + + // table above the sash + Composite topParent = new Composite(mAllocationBase, SWT.NONE); + topParent.setLayout(new GridLayout(2, false)); + + mEnableButton = new Button(topParent, SWT.PUSH); + mEnableButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Client current = getCurrentClient(); + AllocationTrackingStatus status = current.getClientData().getAllocationStatus(); + if (status == AllocationTrackingStatus.ON) { + current.enableAllocationTracker(false); + } else { + current.enableAllocationTracker(true); + } + current.requestAllocationStatus(); + } + }); + + mRequestButton = new Button(topParent, SWT.PUSH); + mRequestButton.setText("Get Allocations"); + mRequestButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + getCurrentClient().requestAllocationDetails(); + } + }); + + setUpButtons(false /* enabled */, AllocationTrackingStatus.OFF); + + mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION); + GridData gridData; + mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH)); + gridData.horizontalSpan = 2; + mAllocationTable.setHeaderVisible(true); + mAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocation Size", + SWT.RIGHT, + "888", //$NON-NLS-1$ + PREFS_ALLOC_COL_SIZE, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated Class", + SWT.LEFT, + "Allocated Class", //$NON-NLS-1$ + PREFS_ALLOC_COL_CLASS, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Thread Id", + SWT.LEFT, + "999", //$NON-NLS-1$ + PREFS_ALLOC_COL_THREAD, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated in", + SWT.LEFT, + "utime", //$NON-NLS-1$ + PREFS_ALLOC_COL_TRACE_CLASS, store); + + TableHelper.createTableColumn( + mAllocationTable, + "Allocated in", + SWT.LEFT, + "utime", //$NON-NLS-1$ + PREFS_ALLOC_COL_TRACE_METHOD, store); + + mAllocationViewer = new TableViewer(mAllocationTable); + mAllocationViewer.setContentProvider(new AllocationContentProvider()); + mAllocationViewer.setLabelProvider(new AllocationLabelProvider()); + + mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + AllocationInfo selectedAlloc = getAllocationSelection(event.getSelection()); + updateAllocationStackTrace(selectedAlloc); + } + }); + + // the separating sash + final Sash sash = new Sash(mAllocationBase, SWT.HORIZONTAL); + Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + sash.setBackground(darkGray); + + // the UI below the sash + mStackTracePanel = new StackTracePanel(); + mStackTraceTable = mStackTracePanel.createPanel(mAllocationBase, + PREFS_STACK_COL_CLASS, + PREFS_STACK_COL_METHOD, + PREFS_STACK_COL_FILE, + PREFS_STACK_COL_LINE, + PREFS_STACK_COL_NATIVE, + store); + + // now setup the sash. + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + topParent.setLayoutData(data); + + final FormData sashData = new FormData(); + if (store != null && store.contains(PREFS_ALLOC_SASH)) { + sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mStackTraceTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = mAllocationBase.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + store.setValue(PREFS_ALLOC_SASH, e.y); + mAllocationBase.layout(); + } + } + }); + + return mAllocationBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mAllocationTable.setFocus(); + } + + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) { + try { + mAllocationTable.getDisplay().asyncExec(new Runnable() { + public void run() { + mAllocationViewer.refresh(); + updateAllocationStackCall(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } else if ((changeMask & Client.CHANGE_HEAP_ALLOCATION_STATUS) != 0) { + try { + mAllocationTable.getDisplay().asyncExec(new Runnable() { + public void run() { + setUpButtons(true, client.getClientData().getAllocationStatus()); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mAllocationTable.isDisposed()) { + return; + } + + Client client = getCurrentClient(); + + mStackTracePanel.setCurrentClient(client); + mStackTracePanel.setViewerInput(null); // always empty on client selection change. + + if (client != null) { + setUpButtons(true /* enabled */, client.getClientData().getAllocationStatus()); + } else { + setUpButtons(false /* enabled */, AllocationTrackingStatus.OFF); + } + + mAllocationViewer.setInput(client); + } + + /** + * Updates the stack call of the currently selected thread. + *

+ * This must be called from the UI thread. + */ + private void updateAllocationStackCall() { + Client client = getCurrentClient(); + if (client != null) { + // get the current selection in the ThreadTable + AllocationInfo selectedAlloc = getAllocationSelection(null); + + if (selectedAlloc != null) { + updateAllocationStackTrace(selectedAlloc); + } else { + updateAllocationStackTrace(null); + } + } + } + + /** + * updates the stackcall of the specified allocation. If null the UI is emptied + * of current data. + * @param thread + */ + private void updateAllocationStackTrace(AllocationInfo alloc) { + mStackTracePanel.setViewerInput(alloc); + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mAllocationTable); + addTableToFocusListener(mStackTraceTable); + } + + /** + * Returns the current allocation selection or null if none is found. + * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this + * selection is returned, otherwise, the ISelection returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or null + */ + private AllocationInfo getAllocationSelection(ISelection selection) { + if (selection == null) { + selection = mAllocationViewer.getSelection(); + } + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof AllocationInfo) { + return (AllocationInfo)object; + } + } + + return null; + } + + /** + * + * @param enabled + * @param trackingStatus + */ + private void setUpButtons(boolean enabled, AllocationTrackingStatus trackingStatus) { + if (enabled) { + switch (trackingStatus) { + case UNKNOWN: + mEnableButton.setText("?"); + mEnableButton.setEnabled(false); + mRequestButton.setEnabled(false); + break; + case OFF: + mEnableButton.setText("Start Tracking"); + mEnableButton.setEnabled(true); + mRequestButton.setEnabled(false); + break; + case ON: + mEnableButton.setText("Stop Tracking"); + mEnableButton.setEnabled(true); + mRequestButton.setEnabled(true); + break; + } + } else { + mEnableButton.setEnabled(false); + mRequestButton.setEnabled(false); + mEnableButton.setText("Start Tracking"); + } + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java new file mode 100644 index 000000000..0ed4c950c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BackgroundThread.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.Log; + +/** + * base background thread class. The class provides a synchronous quit method + * which sets a quitting flag to true. Inheriting classes should regularly test + * this flag with isQuitting() and should finish if the flag is + * true. + */ +public abstract class BackgroundThread extends Thread { + private boolean mQuit = false; + + /** + * Tell the thread to exit. This is usually called from the UI thread. The + * call is synchronous and will only return once the thread has terminated + * itself. + */ + public final void quit() { + mQuit = true; + Log.d("ddms", "Waiting for BackgroundThread to quit"); + try { + this.join(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + + /** returns if the thread was asked to quit. */ + protected final boolean isQuitting() { + return mQuit; + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java new file mode 100644 index 000000000..3e66ea581 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/BaseHeapPanel.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.HeapSegment; +import com.android.ddmlib.ClientData.HeapData; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + + +/** + * Base Panel for heap panels. + */ +public abstract class BaseHeapPanel extends TablePanel { + + /** store the processed heap segment, so that we don't recompute Image for nothing */ + protected byte[] mProcessedHeapData; + private Map> mHeapMap; + + /** + * Serialize the heap data into an array. The resulting array is available through + * getSerializedData(). + * @param heapData The heap data to serialize + * @return true if the data changed. + */ + protected boolean serializeHeapData(HeapData heapData) { + Collection heapSegments; + + // Atomically get and clear the heap data. + synchronized (heapData) { + // get the segments + heapSegments = heapData.getHeapSegments(); + + + if (heapSegments != null) { + // if they are not null, we never processed them. + // Before we process then, we drop them from the HeapData + heapData.clearHeapData(); + + // process them into a linear byte[] + doSerializeHeapData(heapSegments); + heapData.setProcessedHeapData(mProcessedHeapData); + heapData.setProcessedHeapMap(mHeapMap); + + } else { + // the heap segments are null. Let see if the heapData contains a + // list that is already processed. + + byte[] pixData = heapData.getProcessedHeapData(); + + // and compare it to the one we currently have in the panel. + if (pixData == mProcessedHeapData) { + // looks like its the same + return false; + } else { + mProcessedHeapData = pixData; + } + + Map> heapMap = + heapData.getProcessedHeapMap(); + mHeapMap = heapMap; + } + } + + return true; + } + + /** + * Returns the serialized heap data + */ + protected byte[] getSerializedData() { + return mProcessedHeapData; + } + + /** + * Processes and serialize the heapData. + *

+ * The resulting serialized array is {@link #mProcessedHeapData}. + *

+ * the resulting map is {@link #mHeapMap}. + * @param heapData the collection of {@link HeapSegment} that forms the heap data. + */ + private void doSerializeHeapData(Collection heapData) { + mHeapMap = new TreeMap>(); + + Iterator iterator; + ByteArrayOutputStream out; + + out = new ByteArrayOutputStream(4 * 1024); + + iterator = heapData.iterator(); + while (iterator.hasNext()) { + HeapSegment hs = iterator.next(); + + HeapSegmentElement e = null; + while (true) { + int v; + + e = hs.getNextElement(null); + if (e == null) { + break; + } + + if (e.getSolidity() == HeapSegmentElement.SOLIDITY_FREE) { + v = 1; + } else { + v = e.getKind() + 2; + } + + // put the element in the map + ArrayList elementList = mHeapMap.get(v); + if (elementList == null) { + elementList = new ArrayList(); + mHeapMap.put(v, elementList); + } + elementList.add(e); + + + int len = e.getLength() / 8; + while (len > 0) { + out.write(v); + --len; + } + } + } + mProcessedHeapData = out.toByteArray(); + + // sort the segment element in the heap info. + Collection> elementLists = mHeapMap.values(); + for (ArrayList elementList : elementLists) { + Collections.sort(elementList); + } + } + + /** + * Creates a linear image of the heap data. + * @param pixData + * @param h + * @param palette + * @return + */ + protected ImageData createLinearHeapImage(byte[] pixData, int h, PaletteData palette) { + int w = pixData.length / h; + if (pixData.length % h != 0) { + w++; + } + + // Create the heap image. + ImageData id = new ImageData(w, h, 8, palette); + + int x = 0; + int y = 0; + for (byte b : pixData) { + if (b >= 0) { + id.setPixel(x, y, b); + } + + y++; + if (y >= h) { + y = 0; + x++; + } + } + + return id; + } + + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java new file mode 100644 index 000000000..a7119337b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ClientDisplayPanel.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +public abstract class ClientDisplayPanel extends SelectionDependentPanel + implements IClientChangeListener { + + @Override + protected void postCreation() { + AndroidDebugBridge.addClientChangeListener(this); + } + + public void dispose() { + AndroidDebugBridge.removeClientChangeListener(this); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java new file mode 100644 index 000000000..f832a4ee0 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DdmUiPreferences.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2007 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; + +import org.eclipse.jface.preference.IPreferenceStore; + +/** + * Preference entry point for ddmuilib. Allows the lib to access a preference + * store (org.eclipse.jface.preference.IPreferenceStore) defined by the + * application that includes the lib. + */ +public final class DdmUiPreferences { + + public static final int DEFAULT_THREAD_REFRESH_INTERVAL = 4; // seconds + + private static int sThreadRefreshInterval = DEFAULT_THREAD_REFRESH_INTERVAL; + + private static IPreferenceStore mStore; + + private static String sSymbolLocation =""; //$NON-NLS-1$ + private static String sAddr2LineLocation =""; //$NON-NLS-1$ + private static String sTraceviewLocation =""; //$NON-NLS-1$ + + public static void setStore(IPreferenceStore store) { + mStore = store; + } + + public static IPreferenceStore getStore() { + return mStore; + } + + public static int getThreadRefreshInterval() { + return sThreadRefreshInterval; + } + + public static void setThreadRefreshInterval(int port) { + sThreadRefreshInterval = port; + } + + static String getSymbolDirectory() { + return sSymbolLocation; + } + + public static void setSymbolsLocation(String location) { + sSymbolLocation = location; + } + + static String getAddr2Line() { + return sAddr2LineLocation; + } + + public static void setAddr2LineLocation(String location) { + sAddr2LineLocation = location; + } + + public static String getTraceview() { + return sTraceviewLocation; + } + + public static void setTraceviewLocation(String location) { + sTraceviewLocation = location; + } + + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java new file mode 100644 index 000000000..691692f94 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java @@ -0,0 +1,760 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.ClientData.DebuggerStatus; +import com.android.ddmlib.IDevice.DeviceState; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; + +import java.util.ArrayList; + +/** + * A display of both the devices and their clients. + */ +public final class DevicePanel extends Panel implements IDebugBridgeChangeListener, + IDeviceChangeListener, IClientChangeListener { + + private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$ + + private final static int DEVICE_COL_SERIAL = 0; + private final static int DEVICE_COL_STATE = 1; + // col 2, 3 not used. + private final static int DEVICE_COL_BUILD = 4; + + private final static int CLIENT_COL_NAME = 0; + private final static int CLIENT_COL_PID = 1; + private final static int CLIENT_COL_THREAD = 2; + private final static int CLIENT_COL_HEAP = 3; + private final static int CLIENT_COL_PORT = 4; + + public final static int ICON_WIDTH = 16; + public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$ + public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$ + public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$ + public final static String ICON_GC = "gc.png"; //$NON-NLS-1$ + public final static String ICON_HPROF = "hprof.png"; //$NON-NLS-1$ + public final static String ICON_TRACING_START = "tracing_start.png"; //$NON-NLS-1$ + public final static String ICON_TRACING_STOP = "tracing_stop.png"; //$NON-NLS-1$ + + private IDevice mCurrentDevice; + private Client mCurrentClient; + + private Tree mTree; + private TreeViewer mTreeViewer; + + private Image mDeviceImage; + private Image mEmulatorImage; + + private Image mThreadImage; + private Image mHeapImage; + private Image mWaitingImage; + private Image mDebuggerImage; + private Image mDebugErrorImage; + + private final ArrayList mListeners = new ArrayList(); + + private final ArrayList mDevicesToExpand = new ArrayList(); + + private IImageLoader mLoader; + + private boolean mAdvancedPortSupport; + + /** + * A Content provider for the {@link TreeViewer}. + *

+ * The input is a {@link AndroidDebugBridge}. First level elements are {@link IDevice} objects, + * and second level elements are {@link Client} object. + */ + private class ContentProvider implements ITreeContentProvider { + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof IDevice) { + return ((IDevice)parentElement).getClients(); + } + return new Object[0]; + } + + public Object getParent(Object element) { + if (element instanceof Client) { + return ((Client)element).getDevice(); + } + return null; + } + + public boolean hasChildren(Object element) { + if (element instanceof IDevice) { + return ((IDevice)element).hasClients(); + } + + // Clients never have children. + return false; + } + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof AndroidDebugBridge) { + return ((AndroidDebugBridge)inputElement).getDevices(); + } + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + /** + * A Label Provider for the {@link TreeViewer} in {@link DevicePanel}. It provides + * labels and images for {@link IDevice} and {@link Client} objects. + */ + private class LabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + if (columnIndex == DEVICE_COL_SERIAL && element instanceof IDevice) { + IDevice device = (IDevice)element; + if (device.isEmulator()) { + return mEmulatorImage; + } + + return mDeviceImage; + } else if (element instanceof Client) { + Client client = (Client)element; + ClientData cd = client.getClientData(); + + switch (columnIndex) { + case CLIENT_COL_NAME: + switch (cd.getDebuggerConnectionStatus()) { + case DEFAULT: + return null; + case WAITING: + return mWaitingImage; + case ATTACHED: + return mDebuggerImage; + case ERROR: + return mDebugErrorImage; + } + return null; + case CLIENT_COL_THREAD: + if (client.isThreadUpdateEnabled()) { + return mThreadImage; + } + return null; + case CLIENT_COL_HEAP: + if (client.isHeapUpdateEnabled()) { + return mHeapImage; + } + return null; + } + } + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof IDevice) { + IDevice device = (IDevice)element; + switch (columnIndex) { + case DEVICE_COL_SERIAL: + return device.getSerialNumber(); + case DEVICE_COL_STATE: + return getStateString(device); + case DEVICE_COL_BUILD: { + String version = device.getProperty(IDevice.PROP_BUILD_VERSION); + if (version != null) { + String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); + if (device.isEmulator()) { + String avdName = device.getAvdName(); + if (avdName == null) { + avdName = "?"; // the device is probably not online yet, so + // we don't know its AVD name just yet. + } + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return String.format("%1$s [%2$s, debug]", avdName, + version); + } else { + return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$ + } + } else { + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return String.format("%1$s, debug", version); + } else { + return String.format("%1$s", version); //$NON-NLS-1$ + } + } + } else { + return "unknown"; + } + } + } + } else if (element instanceof Client) { + Client client = (Client)element; + ClientData cd = client.getClientData(); + + switch (columnIndex) { + case CLIENT_COL_NAME: + String name = cd.getClientDescription(); + if (name != null) { + return name; + } + return "?"; + case CLIENT_COL_PID: + return Integer.toString(cd.getPid()); + case CLIENT_COL_PORT: + if (mAdvancedPortSupport) { + int port = client.getDebuggerListenPort(); + String portString = "?"; + if (port != 0) { + portString = Integer.toString(port); + } + if (client.isSelectedClient()) { + return String.format("%1$s / %2$d", portString, //$NON-NLS-1$ + DdmPreferences.getSelectedDebugPort()); + } + + return portString; + } + } + } + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Classes which implement this interface provide methods that deals + * with {@link IDevice} and {@link Client} selection changes coming from the ui. + */ + public interface IUiSelectionListener { + /** + * Sent when a new {@link IDevice} and {@link Client} are selected. + * @param selectedDevice the selected device. If null, no devices are selected. + * @param selectedClient The selected client. If null, no clients are selected. + */ + public void selectionChanged(IDevice selectedDevice, Client selectedClient); + } + + /** + * Creates the {@link DevicePanel} object. + * @param loader + * @param advancedPortSupport if true the device panel will add support for selected client port + * and display the ports in the ui. + */ + public DevicePanel(IImageLoader loader, boolean advancedPortSupport) { + mLoader = loader; + mAdvancedPortSupport = advancedPortSupport; + } + + public void addSelectionListener(IUiSelectionListener listener) { + mListeners.add(listener); + } + + public void removeSelectionListener(IUiSelectionListener listener) { + mListeners.remove(listener); + } + + @Override + protected Control createControl(Composite parent) { + loadImages(parent.getDisplay(), mLoader); + + parent.setLayout(new FillLayout()); + + // create the tree and its column + mTree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION); + mTree.setHeaderVisible(true); + mTree.setLinesVisible(true); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, + "com.android.home", //$NON-NLS-1$ + PREFS_COL_NAME_SERIAL, store); + TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$ + "Offline", //$NON-NLS-1$ + PREFS_COL_PID_STATE, store); + + TreeColumn col = new TreeColumn(mTree, SWT.NONE); + col.setWidth(ICON_WIDTH + 8); + col.setResizable(false); + col = new TreeColumn(mTree, SWT.NONE); + col.setWidth(ICON_WIDTH + 8); + col.setResizable(false); + + TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$ + "9999-9999", //$NON-NLS-1$ + PREFS_COL_PORT_BUILD, store); + + // create the tree viewer + mTreeViewer = new TreeViewer(mTree); + + // make the device auto expanded. + mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); + + // set up the content and label providers. + mTreeViewer.setContentProvider(new ContentProvider()); + mTreeViewer.setLabelProvider(new LabelProvider()); + + mTree.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + notifyListeners(); + } + }); + + return mTree; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTree.setFocus(); + } + + @Override + protected void postCreation() { + // ask for notification of changes in AndroidDebugBridge (a new one is created when + // adb is restarted from a different location), IDevice and Client objects. + AndroidDebugBridge.addDebugBridgeChangeListener(this); + AndroidDebugBridge.addDeviceChangeListener(this); + AndroidDebugBridge.addClientChangeListener(this); + } + + public void dispose() { + AndroidDebugBridge.removeDebugBridgeChangeListener(this); + AndroidDebugBridge.removeDeviceChangeListener(this); + AndroidDebugBridge.removeClientChangeListener(this); + } + + /** + * Returns the selected {@link Client}. May be null. + */ + public Client getSelectedClient() { + return mCurrentClient; + } + + /** + * Returns the selected {@link IDevice}. If a {@link Client} is selected, it returns the + * IDevice object containing the client. + */ + public IDevice getSelectedDevice() { + return mCurrentDevice; + } + + /** + * Kills the selected {@link Client} by sending its VM a halt command. + */ + public void killSelectedClient() { + if (mCurrentClient != null) { + Client client = mCurrentClient; + + // reset the selection to the device. + TreePath treePath = new TreePath(new Object[] { mCurrentDevice }); + TreeSelection treeSelection = new TreeSelection(treePath); + mTreeViewer.setSelection(treeSelection); + + client.kill(); + } + } + + /** + * Forces a GC on the selected {@link Client}. + */ + public void forceGcOnSelectedClient() { + if (mCurrentClient != null) { + mCurrentClient.executeGarbageCollector(); + } + } + + public void dumpHprof() { + if (mCurrentClient != null) { + mCurrentClient.dumpHprof(); + } + } + + public void toggleMethodProfiling() { + if (mCurrentClient != null) { + mCurrentClient.toggleMethodProfiling(); + } + } + + public void setEnabledHeapOnSelectedClient(boolean enable) { + if (mCurrentClient != null) { + mCurrentClient.setHeapUpdateEnabled(enable); + } + } + + public void setEnabledThreadOnSelectedClient(boolean enable) { + if (mCurrentClient != null) { + mCurrentClient.setThreadUpdateEnabled(enable); + } + } + + /** + * Sent when a new {@link AndroidDebugBridge} is started. + *

+ * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + * + * @see IDebugBridgeChangeListener#serverChanged(AndroidDebugBridge) + */ + public void bridgeChanged(final AndroidDebugBridge bridge) { + if (mTree.isDisposed() == false) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // set up the data source. + mTreeViewer.setInput(bridge); + + // notify the listener of a possible selection change. + notifyListeners(); + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + // all current devices are obsolete + synchronized (mDevicesToExpand) { + mDevicesToExpand.clear(); + } + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceConnected(IDevice) + */ + public void deviceConnected(IDevice device) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // refresh all + mTreeViewer.refresh(); + + // notify the listener of a possible selection change. + notifyListeners(); + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + + // if it doesn't have clients yet, it'll need to be manually expanded when it gets them. + if (device.hasClients() == false) { + synchronized (mDevicesToExpand) { + mDevicesToExpand.add(device); + } + } + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(IDevice) + */ + public void deviceDisconnected(IDevice device) { + deviceConnected(device); + + // just in case, we remove it from the list of devices to expand. + synchronized (mDevicesToExpand) { + mDevicesToExpand.remove(device); + } + } + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + *

+ * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask indicating what changed. + * + * @see IDeviceChangeListener#deviceChanged(IDevice) + */ + public void deviceChanged(final IDevice device, int changeMask) { + boolean expand = false; + synchronized (mDevicesToExpand) { + int index = mDevicesToExpand.indexOf(device); + if (device.hasClients() && index != -1) { + mDevicesToExpand.remove(index); + expand = true; + } + } + + final boolean finalExpand = expand; + + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // look if the current device is selected. This is done in case the current + // client of this particular device was killed. In this case, we'll need to + // manually reselect the device. + + IDevice selectedDevice = getSelectedDevice(); + + // refresh the device + mTreeViewer.refresh(device); + + // if the selected device was the changed device and the new selection is + // empty, we reselect the device. + if (selectedDevice == device && mTreeViewer.getSelection().isEmpty()) { + mTreeViewer.setSelection(new TreeSelection(new TreePath( + new Object[] { device }))); + } + + // notify the listener of a possible selection change. + notifyListeners(); + + if (finalExpand) { + mTreeViewer.setExpandedState(device, true); + } + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, final int changeMask) { + exec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // refresh the client + mTreeViewer.refresh(client); + + if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == + Client.CHANGE_DEBUGGER_STATUS && + client.getClientData().getDebuggerConnectionStatus() == + DebuggerStatus.WAITING) { + // make sure the device is expanded. Normally the setSelection below + // will auto expand, but the children of device may not already exist + // at this time. Forcing an expand will make the TreeViewer create them. + IDevice device = client.getDevice(); + if (mTreeViewer.getExpandedState(device) == false) { + mTreeViewer.setExpandedState(device, true); + } + + // create and set the selection + TreePath treePath = new TreePath(new Object[] { device, client}); + TreeSelection treeSelection = new TreeSelection(treePath); + mTreeViewer.setSelection(treeSelection); + + if (mAdvancedPortSupport) { + client.setAsSelectedClient(); + } + + // notify the listener of a possible selection change. + notifyListeners(device, client); + } + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + private void loadImages(Display display, IImageLoader loader) { + if (mDeviceImage == null) { + mDeviceImage = ImageHelper.loadImage(loader, display, "device.png", //$NON-NLS-1$ + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } + if (mEmulatorImage == null) { + mEmulatorImage = ImageHelper.loadImage(loader, display, + "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_BLUE)); + } + if (mThreadImage == null) { + mThreadImage = ImageHelper.loadImage(loader, display, ICON_THREAD, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_YELLOW)); + } + if (mHeapImage == null) { + mHeapImage = ImageHelper.loadImage(loader, display, ICON_HEAP, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_BLUE)); + } + if (mWaitingImage == null) { + mWaitingImage = ImageHelper.loadImage(loader, display, + "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_RED)); + } + if (mDebuggerImage == null) { + mDebuggerImage = ImageHelper.loadImage(loader, display, + "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_GREEN)); + } + if (mDebugErrorImage == null) { + mDebugErrorImage = ImageHelper.loadImage(loader, display, + "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_RED)); + } + } + + /** + * Returns a display string representing the state of the device. + * @param d the device + */ + private static String getStateString(IDevice d) { + DeviceState deviceState = d.getState(); + if (deviceState == DeviceState.ONLINE) { + return "Online"; + } else if (deviceState == DeviceState.OFFLINE) { + return "Offline"; + } else if (deviceState == DeviceState.BOOTLOADER) { + return "Bootloader"; + } + + return "??"; + } + + /** + * Executes the {@link Runnable} in the UI thread. + * @param runnable the runnable to execute. + */ + private void exec(Runnable runnable) { + try { + Display display = mTree.getDisplay(); + display.asyncExec(runnable); + } catch (SWTException e) { + // tree is disposed, we need to do something. lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(this); + AndroidDebugBridge.removeDeviceChangeListener(this); + AndroidDebugBridge.removeClientChangeListener(this); + } + } + + private void notifyListeners() { + // get the selection + TreeItem[] items = mTree.getSelection(); + + Client client = null; + IDevice device = null; + + if (items.length == 1) { + Object object = items[0].getData(); + if (object instanceof Client) { + client = (Client)object; + device = client.getDevice(); + } else if (object instanceof IDevice) { + device = (IDevice)object; + } + } + + notifyListeners(device, client); + } + + private void notifyListeners(IDevice selectedDevice, Client selectedClient) { + if (selectedDevice != mCurrentDevice || selectedClient != mCurrentClient) { + mCurrentDevice = selectedDevice; + mCurrentClient = selectedClient; + + for (IUiSelectionListener listener : mListeners) { + // notify the listener with a try/catch-all to make sure this thread won't die + // because of an uncaught exception before all the listeners were notified. + try { + listener.selectionChanged(selectedDevice, selectedClient); + } catch (Exception e) { + } + } + } + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java new file mode 100644 index 000000000..4410d3a3d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/EmulatorControlPanel.java @@ -0,0 +1,1454 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.EmulatorConsole; +import com.android.ddmlib.EmulatorConsole.GsmMode; +import com.android.ddmlib.EmulatorConsole.GsmStatus; +import com.android.ddmlib.EmulatorConsole.NetworkStatus; +import com.android.ddmuilib.location.CoordinateControls; +import com.android.ddmuilib.location.GpxParser; +import com.android.ddmuilib.location.KmlParser; +import com.android.ddmuilib.location.TrackContentProvider; +import com.android.ddmuilib.location.TrackLabelProvider; +import com.android.ddmuilib.location.TrackPoint; +import com.android.ddmuilib.location.WayPoint; +import com.android.ddmuilib.location.WayPointContentProvider; +import com.android.ddmuilib.location.WayPointLabelProvider; +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +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.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.Text; + +/** + * Panel to control the emulator using EmulatorConsole objects. + */ +public class EmulatorControlPanel extends SelectionDependentPanel { + + // default location: Patio outside Charlie's + private final static double DEFAULT_LONGITUDE = -122.084095; + private final static double DEFAULT_LATITUDE = 37.422006; + + private final static String SPEED_FORMAT = "Speed: %1$dX"; + + + /** + * Map between the display gsm mode and the internal tag used by the display. + */ + private final static String[][] GSM_MODES = new String[][] { + { "unregistered", GsmMode.UNREGISTERED.getTag() }, + { "home", GsmMode.HOME.getTag() }, + { "roaming", GsmMode.ROAMING.getTag() }, + { "searching", GsmMode.SEARCHING.getTag() }, + { "denied", GsmMode.DENIED.getTag() }, + }; + + private final static String[] NETWORK_SPEEDS = new String[] { + "Full", + "GSM", + "HSCSD", + "GPRS", + "EDGE", + "UMTS", + "HSDPA", + }; + + private final static String[] NETWORK_LATENCIES = new String[] { + "None", + "GPRS", + "EDGE", + "UMTS", + }; + + private final static int[] PLAY_SPEEDS = new int[] { 1, 2, 5, 10, 20, 50 }; + + private final static String RE_PHONE_NUMBER = "^[+#0-9]+$"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_NAME = "emulatorControl.waypoint.name"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_LONGITUDE = "emulatorControl.waypoint.longitude"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_LATITUDE = "emulatorControl.waypoint.latitude"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_ELEVATION = "emulatorControl.waypoint.elevation"; //$NON-NLS-1$ + private final static String PREFS_WAYPOINT_COL_DESCRIPTION = "emulatorControl.waypoint.desc"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_NAME = "emulatorControl.track.name"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_COUNT = "emulatorControl.track.count"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_FIRST = "emulatorControl.track.first"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_LAST = "emulatorControl.track.last"; //$NON-NLS-1$ + private final static String PREFS_TRACK_COL_COMMENT = "emulatorControl.track.comment"; //$NON-NLS-1$ + + private IImageLoader mImageLoader; + + private EmulatorConsole mEmulatorConsole; + + private Composite mParent; + + private Label mVoiceLabel; + private Combo mVoiceMode; + private Label mDataLabel; + private Combo mDataMode; + private Label mSpeedLabel; + private Combo mNetworkSpeed; + private Label mLatencyLabel; + private Combo mNetworkLatency; + + private Label mNumberLabel; + private Text mPhoneNumber; + + private Button mVoiceButton; + private Button mSmsButton; + + private Label mMessageLabel; + private Text mSmsMessage; + + private Button mCallButton; + private Button mCancelButton; + + private TabFolder mLocationFolders; + + private Button mDecimalButton; + private Button mSexagesimalButton; + private CoordinateControls mLongitudeControls; + private CoordinateControls mLatitudeControls; + private Button mGpxUploadButton; + private Table mGpxWayPointTable; + private Table mGpxTrackTable; + private Button mKmlUploadButton; + private Table mKmlWayPointTable; + + private Button mPlayGpxButton; + private Button mGpxBackwardButton; + private Button mGpxForwardButton; + private Button mGpxSpeedButton; + private Button mPlayKmlButton; + private Button mKmlBackwardButton; + private Button mKmlForwardButton; + private Button mKmlSpeedButton; + + private Image mPlayImage; + private Image mPauseImage; + + private Thread mPlayingThread; + private boolean mPlayingTrack; + private int mPlayDirection = 1; + private int mSpeed; + private int mSpeedIndex; + + private final SelectionAdapter mDirectionButtonAdapter = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Button b = (Button)e.getSource(); + if (b.getSelection() == false) { + // basically the button was unselected, which we don't allow. + // so we reselect it. + b.setSelection(true); + return; + } + + // now handle selection change. + if (b == mGpxForwardButton || b == mKmlForwardButton) { + mGpxBackwardButton.setSelection(false); + mGpxForwardButton.setSelection(true); + mKmlBackwardButton.setSelection(false); + mKmlForwardButton.setSelection(true); + mPlayDirection = 1; + + } else { + mGpxBackwardButton.setSelection(true); + mGpxForwardButton.setSelection(false); + mKmlBackwardButton.setSelection(true); + mKmlForwardButton.setSelection(false); + mPlayDirection = -1; + } + } + }; + + private final SelectionAdapter mSpeedButtonAdapter = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mSpeedIndex = (mSpeedIndex+1) % PLAY_SPEEDS.length; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mGpxPlayControls.pack(); + mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mKmlPlayControls.pack(); + + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + }; + private Composite mKmlPlayControls; + private Composite mGpxPlayControls; + + + public EmulatorControlPanel(IImageLoader imageLoader) { + mImageLoader = imageLoader; + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + handleNewDevice(getCurrentDevice()); + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()} + */ + @Override + public void clientSelected() { + // pass + } + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + + final ScrolledComposite scollingParent = new ScrolledComposite(parent, SWT.V_SCROLL); + scollingParent.setExpandVertical(true); + scollingParent.setExpandHorizontal(true); + scollingParent.setLayoutData(new GridData(GridData.FILL_BOTH)); + + final Composite top = new Composite(scollingParent, SWT.NONE); + scollingParent.setContent(top); + top.setLayout(new GridLayout(1, false)); + + // set the resize for the scrolling to work (why isn't that done automatically?!?) + scollingParent.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = scollingParent.getClientArea(); + scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT)); + } + }); + + createRadioControls(top); + + createCallControls(top); + + createLocationControls(top); + + doEnable(false); + + top.layout(); + Rectangle r = scollingParent.getClientArea(); + scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT)); + + return scollingParent; + } + + /** + * Create Radio (on/off/roaming, for voice/data) controls. + * @param top + */ + private void createRadioControls(final Composite top) { + Group g1 = new Group(top, SWT.NONE); + g1.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + g1.setLayout(new GridLayout(2, false)); + g1.setText("Telephony Status"); + + // the inside of the group is 2 composite so that all the column of the controls (mainly + // combos) have the same width, while not taking the whole screen width + Composite insideGroup = new Composite(g1, SWT.NONE); + GridLayout gl = new GridLayout(4, false); + gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0; + insideGroup.setLayout(gl); + + mVoiceLabel = new Label(insideGroup, SWT.NONE); + mVoiceLabel.setText("Voice:"); + mVoiceLabel.setAlignment(SWT.RIGHT); + + mVoiceMode = new Combo(insideGroup, SWT.READ_ONLY); + mVoiceMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String[] mode : GSM_MODES) { + mVoiceMode.add(mode[0]); + } + mVoiceMode.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setVoiceMode(mVoiceMode.getSelectionIndex()); + } + }); + + mSpeedLabel = new Label(insideGroup, SWT.NONE); + mSpeedLabel.setText("Speed:"); + mSpeedLabel.setAlignment(SWT.RIGHT); + + mNetworkSpeed = new Combo(insideGroup, SWT.READ_ONLY); + mNetworkSpeed.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String mode : NETWORK_SPEEDS) { + mNetworkSpeed.add(mode); + } + mNetworkSpeed.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setNetworkSpeed(mNetworkSpeed.getSelectionIndex()); + } + }); + + mDataLabel = new Label(insideGroup, SWT.NONE); + mDataLabel.setText("Data:"); + mDataLabel.setAlignment(SWT.RIGHT); + + mDataMode = new Combo(insideGroup, SWT.READ_ONLY); + mDataMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String[] mode : GSM_MODES) { + mDataMode.add(mode[0]); + } + mDataMode.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setDataMode(mDataMode.getSelectionIndex()); + } + }); + + mLatencyLabel = new Label(insideGroup, SWT.NONE); + mLatencyLabel.setText("Latency:"); + mLatencyLabel.setAlignment(SWT.RIGHT); + + mNetworkLatency = new Combo(insideGroup, SWT.READ_ONLY); + mNetworkLatency.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (String mode : NETWORK_LATENCIES) { + mNetworkLatency.add(mode); + } + mNetworkLatency.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + setNetworkLatency(mNetworkLatency.getSelectionIndex()); + } + }); + + // now an empty label to take the rest of the width of the group + Label l = new Label(g1, SWT.NONE); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + } + + /** + * Create Voice/SMS call/hang up controls + * @param top + */ + private void createCallControls(final Composite top) { + GridLayout gl; + Group g2 = new Group(top, SWT.NONE); + g2.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + g2.setLayout(new GridLayout(1, false)); + g2.setText("Telephony Actions"); + + // horizontal composite for label + text field + Composite phoneComp = new Composite(g2, SWT.NONE); + phoneComp.setLayoutData(new GridData(GridData.FILL_BOTH)); + gl = new GridLayout(2, false); + gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0; + phoneComp.setLayout(gl); + + mNumberLabel = new Label(phoneComp, SWT.NONE); + mNumberLabel.setText("Incoming number:"); + + mPhoneNumber = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + mPhoneNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mPhoneNumber.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + // Reenable the widgets based on the content of the text. + // doEnable checks the validity of the phone number to enable/disable some + // widgets. + // Looks like we're getting a callback at creation time, so we can't + // suppose that we are enabled when the text is modified... + doEnable(mEmulatorConsole != null); + } + }); + + mVoiceButton = new Button(phoneComp, SWT.RADIO); + GridData gd = new GridData(); + gd.horizontalSpan = 2; + mVoiceButton.setText("Voice"); + mVoiceButton.setLayoutData(gd); + mVoiceButton.setEnabled(false); + mVoiceButton.setSelection(true); + mVoiceButton.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + doEnable(true); + + if (mVoiceButton.getSelection()) { + mCallButton.setText("Call"); + } else { + mCallButton.setText("Send"); + } + } + }); + + mSmsButton = new Button(phoneComp, SWT.RADIO); + mSmsButton.setText("SMS"); + gd = new GridData(); + gd.horizontalSpan = 2; + mSmsButton.setLayoutData(gd); + mSmsButton.setEnabled(false); + // Since there are only 2 radio buttons, we can put a listener on only one (they + // are both called on select and unselect event. + + mMessageLabel = new Label(phoneComp, SWT.NONE); + gd = new GridData(); + gd.verticalAlignment = SWT.TOP; + mMessageLabel.setLayoutData(gd); + mMessageLabel.setText("Message:"); + mMessageLabel.setEnabled(false); + + mSmsMessage = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL); + mSmsMessage.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 70; + mSmsMessage.setEnabled(false); + + // composite to put the 2 buttons horizontally + Composite g2ButtonComp = new Composite(g2, SWT.NONE); + g2ButtonComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + gl = new GridLayout(2, false); + gl.marginWidth = gl.marginHeight = 0; + g2ButtonComp.setLayout(gl); + + // now a button below the phone number + mCallButton = new Button(g2ButtonComp, SWT.PUSH); + mCallButton.setText("Call"); + mCallButton.setEnabled(false); + mCallButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + if (mVoiceButton.getSelection()) { + processCommandResult(mEmulatorConsole.call(mPhoneNumber.getText().trim())); + } else { + // we need to encode the message. We need to replace the carriage return + // character by the 2 character string \n. + // Because of this the \ character needs to be escaped as well. + // ReplaceAll() expects regexp so \ char are escaped twice. + String message = mSmsMessage.getText(); + message = message.replaceAll("\\\\", //$NON-NLS-1$ + "\\\\\\\\"); //$NON-NLS-1$ + + // While the normal line delimiter is returned by Text.getLineDelimiter() + // it seems copy pasting text coming from somewhere else could have another + // delimited. For this reason, we'll replace is several steps + + // replace the dual CR-LF + message = message.replaceAll("\r\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + // replace remaining stand alone \n + message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + // replace remaining stand alone \r + message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-1$ + + processCommandResult(mEmulatorConsole.sendSms(mPhoneNumber.getText().trim(), + message)); + } + } + } + }); + + mCancelButton = new Button(g2ButtonComp, SWT.PUSH); + mCancelButton.setText("Hang Up"); + mCancelButton.setEnabled(false); + mCancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + if (mVoiceButton.getSelection()) { + processCommandResult(mEmulatorConsole.cancelCall( + mPhoneNumber.getText().trim())); + } + } + } + }); + } + + /** + * Create Location controls. + * @param top + */ + private void createLocationControls(final Composite top) { + Label l = new Label(top, SWT.NONE); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + l.setText("Location Controls"); + + mLocationFolders = new TabFolder(top, SWT.NONE); + mLocationFolders.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Composite manualLocationComp = new Composite(mLocationFolders, SWT.NONE); + TabItem item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("Manual"); + item.setControl(manualLocationComp); + + createManualLocationControl(manualLocationComp); + + mPlayImage = mImageLoader.loadImage("play.png", mParent.getDisplay()); // $NON-NLS-1$ + mPauseImage = mImageLoader.loadImage("pause.png", mParent.getDisplay()); // $NON-NLS-1$ + + Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE); + item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("GPX"); + item.setControl(gpxLocationComp); + + createGpxLocationControl(gpxLocationComp); + + Composite kmlLocationComp = new Composite(mLocationFolders, SWT.NONE); + kmlLocationComp.setLayout(new FillLayout()); + item = new TabItem(mLocationFolders, SWT.NONE); + item.setText("KML"); + item.setControl(kmlLocationComp); + + createKmlLocationControl(kmlLocationComp); + } + + private void createManualLocationControl(Composite manualLocationComp) { + final StackLayout sl; + GridLayout gl; + Label label; + + manualLocationComp.setLayout(new GridLayout(1, false)); + mDecimalButton = new Button(manualLocationComp, SWT.RADIO); + mDecimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDecimalButton.setText("Decimal"); + mSexagesimalButton = new Button(manualLocationComp, SWT.RADIO); + mSexagesimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSexagesimalButton.setText("Sexagesimal"); + + // composite to hold and switching between the 2 modes. + final Composite content = new Composite(manualLocationComp, SWT.NONE); + content.setLayout(sl = new StackLayout()); + + // decimal display + final Composite decimalContent = new Composite(content, SWT.NONE); + decimalContent.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mLongitudeControls = new CoordinateControls(); + mLatitudeControls = new CoordinateControls(); + + label = new Label(decimalContent, SWT.NONE); + label.setText("Longitude"); + + mLongitudeControls.createDecimalText(decimalContent); + + label = new Label(decimalContent, SWT.NONE); + label.setText("Latitude"); + + mLatitudeControls.createDecimalText(decimalContent); + + // sexagesimal content + final Composite sexagesimalContent = new Composite(content, SWT.NONE); + sexagesimalContent.setLayout(gl = new GridLayout(7, false)); + gl.marginHeight = gl.marginWidth = 0; + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("Longitude"); + + mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\u00B0"); // degree character + + mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("'"); + + mLongitudeControls.createSexagesimalSecondText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\""); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("Latitude"); + + mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\u00B0"); + + mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("'"); + + mLatitudeControls.createSexagesimalSecondText(sexagesimalContent); + + label = new Label(sexagesimalContent, SWT.NONE); + label.setText("\""); + + // set the default display to decimal + sl.topControl = decimalContent; + mDecimalButton.setSelection(true); + + mDecimalButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mDecimalButton.getSelection()) { + sl.topControl = decimalContent; + } else { + sl.topControl = sexagesimalContent; + } + content.layout(); + } + }); + + Button sendButton = new Button(manualLocationComp, SWT.PUSH); + sendButton.setText("Send"); + sendButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.sendLocation( + mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0)); + } + } + }); + + mLongitudeControls.setValue(DEFAULT_LONGITUDE); + mLatitudeControls.setValue(DEFAULT_LATITUDE); + } + + private void createGpxLocationControl(Composite gpxLocationComp) { + GridData gd; + + IPreferenceStore store = DdmUiPreferences.getStore(); + + gpxLocationComp.setLayout(new GridLayout(1, false)); + + mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH); + mGpxUploadButton.setText("Load GPX..."); + + // Table for way point + mGpxWayPointTable = new Table(gpxLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 100; + mGpxWayPointTable.setHeaderVisible(true); + mGpxWayPointTable.setLinesVisible(true); + + TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT, + "Some Name", + PREFS_WAYPOINT_COL_NAME, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LONGITUDE, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LATITUDE, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT, + "99999.9", + PREFS_WAYPOINT_COL_ELEVATION, store); + TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT, + "Some Description", + PREFS_WAYPOINT_COL_DESCRIPTION, store); + + final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable); + gpxWayPointViewer.setContentProvider(new WayPointContentProvider()); + gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider()); + + gpxWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + processCommandResult(mEmulatorConsole.sendLocation( + wayPoint.getLongitude(), wayPoint.getLatitude(), + wayPoint.getElevation())); + } + } + } + } + }); + + // table for tracks. + mGpxTrackTable = new Table(gpxLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mGpxTrackTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 100; + mGpxTrackTable.setHeaderVisible(true); + mGpxTrackTable.setLinesVisible(true); + + TableHelper.createTableColumn(mGpxTrackTable, "Name", SWT.LEFT, + "Some very long name", + PREFS_TRACK_COL_NAME, store); + TableHelper.createTableColumn(mGpxTrackTable, "Point Count", SWT.RIGHT, + "9999", + PREFS_TRACK_COL_COUNT, store); + TableHelper.createTableColumn(mGpxTrackTable, "First Point Time", SWT.LEFT, + "999-99-99T99:99:99Z", + PREFS_TRACK_COL_FIRST, store); + TableHelper.createTableColumn(mGpxTrackTable, "Last Point Time", SWT.LEFT, + "999-99-99T99:99:99Z", + PREFS_TRACK_COL_LAST, store); + TableHelper.createTableColumn(mGpxTrackTable, "Comment", SWT.LEFT, + "-199.999999", + PREFS_TRACK_COL_COMMENT, store); + + final TableViewer gpxTrackViewer = new TableViewer(mGpxTrackTable); + gpxTrackViewer.setContentProvider(new TrackContentProvider()); + gpxTrackViewer.setLabelProvider(new TrackLabelProvider()); + + gpxTrackViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof Track) { + Track track = (Track)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + TrackPoint[] points = track.getPoints(); + processCommandResult(mEmulatorConsole.sendLocation( + points[0].getLongitude(), points[0].getLatitude(), + points[0].getElevation())); + } + + mPlayGpxButton.setEnabled(true); + mGpxBackwardButton.setEnabled(true); + mGpxForwardButton.setEnabled(true); + mGpxSpeedButton.setEnabled(true); + + return; + } + } + + mPlayGpxButton.setEnabled(false); + mGpxBackwardButton.setEnabled(false); + mGpxForwardButton.setEnabled(false); + mGpxSpeedButton.setEnabled(false); + } + }); + + mGpxUploadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Load GPX File"); + fileDialog.setFilterExtensions(new String[] { "*.gpx" } ); + + String fileName = fileDialog.open(); + if (fileName != null) { + GpxParser parser = new GpxParser(fileName); + if (parser.parse()) { + gpxWayPointViewer.setInput(parser.getWayPoints()); + gpxTrackViewer.setInput(parser.getTracks()); + } + } + } + }); + + mGpxPlayControls = new Composite(gpxLocationComp, SWT.NONE); + GridLayout gl; + mGpxPlayControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + mGpxPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mPlayGpxButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT); + mPlayGpxButton.setImage(mPlayImage); + mPlayGpxButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mPlayingTrack == false) { + ISelection selection = gpxTrackViewer.getSelection(); + if (selection.isEmpty() == false && selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof Track) { + Track track = (Track)selectedObject; + playTrack(track); + } + } + } else { + // if we're playing, then we pause + mPlayingTrack = false; + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + } + }); + + Label separator = new Label(mGpxPlayControls, SWT.SEPARATOR | SWT.VERTICAL); + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + + mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$ + mGpxBackwardButton.setSelection(false); + mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxForwardButton.setImage(mImageLoader.loadImage("forward.png", mParent.getDisplay())); // $NON-NLS-1$ + mGpxForwardButton.setSelection(true); + mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter); + + mGpxSpeedButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT); + + mSpeedIndex = 0; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mGpxSpeedButton.addSelectionListener(mSpeedButtonAdapter); + + mPlayGpxButton.setEnabled(false); + mGpxBackwardButton.setEnabled(false); + mGpxForwardButton.setEnabled(false); + mGpxSpeedButton.setEnabled(false); + + } + + private void createKmlLocationControl(Composite kmlLocationComp) { + GridData gd; + + IPreferenceStore store = DdmUiPreferences.getStore(); + + kmlLocationComp.setLayout(new GridLayout(1, false)); + + mKmlUploadButton = new Button(kmlLocationComp, SWT.PUSH); + mKmlUploadButton.setText("Load KML..."); + + // Table for way point + mKmlWayPointTable = new Table(kmlLocationComp, + SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); + mKmlWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.heightHint = 200; + mKmlWayPointTable.setHeaderVisible(true); + mKmlWayPointTable.setLinesVisible(true); + + TableHelper.createTableColumn(mKmlWayPointTable, "Name", SWT.LEFT, + "Some Name", + PREFS_WAYPOINT_COL_NAME, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Longitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LONGITUDE, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Latitude", SWT.LEFT, + "-199.999999", + PREFS_WAYPOINT_COL_LATITUDE, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Elevation", SWT.LEFT, + "99999.9", + PREFS_WAYPOINT_COL_ELEVATION, store); + TableHelper.createTableColumn(mKmlWayPointTable, "Description", SWT.LEFT, + "Some Description", + PREFS_WAYPOINT_COL_DESCRIPTION, store); + + final TableViewer kmlWayPointViewer = new TableViewer(mKmlWayPointTable); + kmlWayPointViewer.setContentProvider(new WayPointContentProvider()); + kmlWayPointViewer.setLabelProvider(new WayPointLabelProvider()); + + mKmlUploadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); + + fileDialog.setText("Load KML File"); + fileDialog.setFilterExtensions(new String[] { "*.kml" } ); + + String fileName = fileDialog.open(); + if (fileName != null) { + KmlParser parser = new KmlParser(fileName); + if (parser.parse()) { + kmlWayPointViewer.setInput(parser.getWayPoints()); + + mPlayKmlButton.setEnabled(true); + mKmlBackwardButton.setEnabled(true); + mKmlForwardButton.setEnabled(true); + mKmlSpeedButton.setEnabled(true); + } + } + } + }); + + kmlWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object selectedObject = structuredSelection.getFirstElement(); + if (selectedObject instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)selectedObject; + + if (mEmulatorConsole != null && mPlayingTrack == false) { + processCommandResult(mEmulatorConsole.sendLocation( + wayPoint.getLongitude(), wayPoint.getLatitude(), + wayPoint.getElevation())); + } + } + } + } + }); + + + + mKmlPlayControls = new Composite(kmlLocationComp, SWT.NONE); + GridLayout gl; + mKmlPlayControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + mKmlPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mPlayKmlButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT); + mPlayKmlButton.setImage(mPlayImage); + mPlayKmlButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mPlayingTrack == false) { + Object input = kmlWayPointViewer.getInput(); + if (input instanceof WayPoint[]) { + playKml((WayPoint[])input); + } + } else { + // if we're playing, then we pause + mPlayingTrack = false; + if (mPlayingThread != null) { + mPlayingThread.interrupt(); + } + } + } + }); + + Label separator = new Label(mKmlPlayControls, SWT.SEPARATOR | SWT.VERTICAL); + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + + mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlBackwardButton.setImage(mImageLoader.loadImage("backward.png", mParent.getDisplay())); // $NON-NLS-1$ + mKmlBackwardButton.setSelection(false); + mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlForwardButton.setImage(mImageLoader.loadImage("forward.png", mParent.getDisplay())); // $NON-NLS-1$ + mKmlForwardButton.setSelection(true); + mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter); + + mKmlSpeedButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT); + + mSpeedIndex = 0; + mSpeed = PLAY_SPEEDS[mSpeedIndex]; + + mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); + mKmlSpeedButton.addSelectionListener(mSpeedButtonAdapter); + + mPlayKmlButton.setEnabled(false); + mKmlBackwardButton.setEnabled(false); + mKmlForwardButton.setEnabled(false); + mKmlSpeedButton.setEnabled(false); + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + } + + @Override + protected void postCreation() { + // pass + } + + private synchronized void setDataMode(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setGsmDataMode( + GsmMode.getEnum(GSM_MODES[selectionIndex][1]))); + } + } + + private synchronized void setVoiceMode(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setGsmVoiceMode( + GsmMode.getEnum(GSM_MODES[selectionIndex][1]))); + } + } + + private synchronized void setNetworkLatency(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setNetworkLatency(selectionIndex)); + } + } + + private synchronized void setNetworkSpeed(int selectionIndex) { + if (mEmulatorConsole != null) { + processCommandResult(mEmulatorConsole.setNetworkSpeed(selectionIndex)); + } + } + + + /** + * Callback on device selection change. + * @param device the new selected device + */ + public void handleNewDevice(IDevice device) { + if (mParent.isDisposed()) { + return; + } + // unlink to previous console. + synchronized (this) { + mEmulatorConsole = null; + } + + try { + // get the emulator console for this device + // First we need the device itself + if (device != null) { + GsmStatus gsm = null; + NetworkStatus netstatus = null; + + synchronized (this) { + mEmulatorConsole = EmulatorConsole.getConsole(device); + if (mEmulatorConsole != null) { + // get the gsm status + gsm = mEmulatorConsole.getGsmStatus(); + netstatus = mEmulatorConsole.getNetworkStatus(); + + if (gsm == null || netstatus == null) { + mEmulatorConsole = null; + } + } + } + + if (gsm != null && netstatus != null) { + Display d = mParent.getDisplay(); + if (d.isDisposed() == false) { + final GsmStatus f_gsm = gsm; + final NetworkStatus f_netstatus = netstatus; + + d.asyncExec(new Runnable() { + public void run() { + if (f_gsm.voice != GsmMode.UNKNOWN) { + mVoiceMode.select(getGsmComboIndex(f_gsm.voice)); + } else { + mVoiceMode.clearSelection(); + } + if (f_gsm.data != GsmMode.UNKNOWN) { + mDataMode.select(getGsmComboIndex(f_gsm.data)); + } else { + mDataMode.clearSelection(); + } + + if (f_netstatus.speed != -1) { + mNetworkSpeed.select(f_netstatus.speed); + } else { + mNetworkSpeed.clearSelection(); + } + + if (f_netstatus.latency != -1) { + mNetworkLatency.select(f_netstatus.latency); + } else { + mNetworkLatency.clearSelection(); + } + } + }); + } + } + } + } finally { + // enable/disable the ui + boolean enable = false; + synchronized (this) { + enable = mEmulatorConsole != null; + } + + enable(enable); + } + } + + /** + * Enable or disable the ui. Can be called from non ui threads. + * @param enabled + */ + private void enable(final boolean enabled) { + try { + Display d = mParent.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + if (mParent.isDisposed() == false) { + doEnable(enabled); + } + } + }); + } catch (SWTException e) { + // disposed. do nothing + } + } + + private boolean isValidPhoneNumber() { + String number = mPhoneNumber.getText().trim(); + + return number.matches(RE_PHONE_NUMBER); + } + + /** + * Enable or disable the ui. Cannot be called from non ui threads. + * @param enabled + */ + protected void doEnable(boolean enabled) { + mVoiceLabel.setEnabled(enabled); + mVoiceMode.setEnabled(enabled); + + mDataLabel.setEnabled(enabled); + mDataMode.setEnabled(enabled); + + mSpeedLabel.setEnabled(enabled); + mNetworkSpeed.setEnabled(enabled); + + mLatencyLabel.setEnabled(enabled); + mNetworkLatency.setEnabled(enabled); + + // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it + // if we don't need to. + if (mPhoneNumber.isEnabled() != enabled) { + mNumberLabel.setEnabled(enabled); + mPhoneNumber.setEnabled(enabled); + } + + boolean valid = isValidPhoneNumber(); + + mVoiceButton.setEnabled(enabled && valid); + mSmsButton.setEnabled(enabled && valid); + + boolean smsValid = enabled && valid && mSmsButton.getSelection(); + + // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it + // if we don't need to. + if (mSmsMessage.isEnabled() != smsValid) { + mMessageLabel.setEnabled(smsValid); + mSmsMessage.setEnabled(smsValid); + } + if (enabled == false) { + mSmsMessage.setText(""); //$NON-NLs-1$ + } + + mCallButton.setEnabled(enabled && valid); + mCancelButton.setEnabled(enabled && valid && mVoiceButton.getSelection()); + + if (enabled == false) { + mVoiceMode.clearSelection(); + mDataMode.clearSelection(); + mNetworkSpeed.clearSelection(); + mNetworkLatency.clearSelection(); + if (mPhoneNumber.getText().length() > 0) { + mPhoneNumber.setText(""); //$NON-NLS-1$ + } + } + + // location controls + mLocationFolders.setEnabled(enabled); + + mDecimalButton.setEnabled(enabled); + mSexagesimalButton.setEnabled(enabled); + mLongitudeControls.setEnabled(enabled); + mLatitudeControls.setEnabled(enabled); + + mGpxUploadButton.setEnabled(enabled); + mGpxWayPointTable.setEnabled(enabled); + mGpxTrackTable.setEnabled(enabled); + mKmlUploadButton.setEnabled(enabled); + mKmlWayPointTable.setEnabled(enabled); + } + + /** + * Returns the index of the combo item matching a specific GsmMode. + * @param mode + */ + private int getGsmComboIndex(GsmMode mode) { + for (int i = 0 ; i < GSM_MODES.length; i++) { + String[] modes = GSM_MODES[i]; + if (mode.getTag().equals(modes[1])) { + return i; + } + } + return -1; + } + + /** + * Processes the result of a command sent to the console. + * @param result the result of the command. + */ + private boolean processCommandResult(final String result) { + if (result != EmulatorConsole.RESULT_OK) { + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mParent.isDisposed() == false) { + MessageDialog.openError(mParent.getShell(), "Emulator Console", + result); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + + return false; + } + + return true; + } + + /** + * @param track + */ + private void playTrack(final Track track) { + // no need to synchronize this check, the worst that can happen, is we start the thread + // for nothing. + if (mEmulatorConsole != null) { + mPlayGpxButton.setImage(mPauseImage); + mPlayKmlButton.setImage(mPauseImage); + mPlayingTrack = true; + + mPlayingThread = new Thread() { + @Override + public void run() { + try { + TrackPoint[] trackPoints = track.getPoints(); + int count = trackPoints.length; + + // get the start index. + int start = 0; + if (mPlayDirection == -1) { + start = count - 1; + } + + for (int p = start; p >= 0 && p < count; p += mPlayDirection) { + if (mPlayingTrack == false) { + return; + } + + // get the current point and send its location to + // the emulator. + final TrackPoint trackPoint = trackPoints[p]; + + synchronized (EmulatorControlPanel.this) { + if (mEmulatorConsole == null || + processCommandResult(mEmulatorConsole.sendLocation( + trackPoint.getLongitude(), trackPoint.getLatitude(), + trackPoint.getElevation())) == false) { + return; + } + } + + // if this is not the final point, then get the next one and + // compute the delta time + int nextIndex = p + mPlayDirection; + if (nextIndex >=0 && nextIndex < count) { + TrackPoint nextPoint = trackPoints[nextIndex]; + + long delta = nextPoint.getTime() - trackPoint.getTime(); + if (delta < 0) { + delta = -delta; + } + + long startTime = System.currentTimeMillis(); + + try { + sleep(delta / mSpeed); + } catch (InterruptedException e) { + if (mPlayingTrack == false) { + return; + } + + // we got interrupted, lets make sure we can play + do { + long waited = System.currentTimeMillis() - startTime; + long needToWait = delta / mSpeed; + if (waited < needToWait) { + try { + sleep(needToWait - waited); + } catch (InterruptedException e1) { + // we'll just loop and wait again if needed. + // unless we're supposed to stop + if (mPlayingTrack == false) { + return; + } + } + } else { + break; + } + } while (true); + } + } + } + } finally { + mPlayingTrack = false; + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mPlayGpxButton.isDisposed() == false) { + mPlayGpxButton.setImage(mPlayImage); + mPlayKmlButton.setImage(mPlayImage); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + } + } + }; + + mPlayingThread.start(); + } + } + + private void playKml(final WayPoint[] trackPoints) { + // no need to synchronize this check, the worst that can happen, is we start the thread + // for nothing. + if (mEmulatorConsole != null) { + mPlayGpxButton.setImage(mPauseImage); + mPlayKmlButton.setImage(mPauseImage); + mPlayingTrack = true; + + mPlayingThread = new Thread() { + @Override + public void run() { + try { + int count = trackPoints.length; + + // get the start index. + int start = 0; + if (mPlayDirection == -1) { + start = count - 1; + } + + for (int p = start; p >= 0 && p < count; p += mPlayDirection) { + if (mPlayingTrack == false) { + return; + } + + // get the current point and send its location to + // the emulator. + WayPoint trackPoint = trackPoints[p]; + + synchronized (EmulatorControlPanel.this) { + if (mEmulatorConsole == null || + processCommandResult(mEmulatorConsole.sendLocation( + trackPoint.getLongitude(), trackPoint.getLatitude(), + trackPoint.getElevation())) == false) { + return; + } + } + + // if this is not the final point, then get the next one and + // compute the delta time + int nextIndex = p + mPlayDirection; + if (nextIndex >=0 && nextIndex < count) { + + long delta = 1000; // 1 second + if (delta < 0) { + delta = -delta; + } + + long startTime = System.currentTimeMillis(); + + try { + sleep(delta / mSpeed); + } catch (InterruptedException e) { + if (mPlayingTrack == false) { + return; + } + + // we got interrupted, lets make sure we can play + do { + long waited = System.currentTimeMillis() - startTime; + long needToWait = delta / mSpeed; + if (waited < needToWait) { + try { + sleep(needToWait - waited); + } catch (InterruptedException e1) { + // we'll just loop and wait again if needed. + // unless we're supposed to stop + if (mPlayingTrack == false) { + return; + } + } + } else { + break; + } + } while (true); + } + } + } + } finally { + mPlayingTrack = false; + try { + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mPlayGpxButton.isDisposed() == false) { + mPlayGpxButton.setImage(mPlayImage); + mPlayKmlButton.setImage(mPlayImage); + } + } + }); + } catch (SWTException e) { + // we're quitting, just ignore + } + } + } + }; + + mPlayingThread.start(); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java new file mode 100644 index 000000000..f5cf9b119 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/HeapPanel.java @@ -0,0 +1,1294 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.Log; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.CategoryAxis; +import org.jfree.chart.axis.CategoryLabelPositions; +import org.jfree.chart.labels.CategoryToolTipGenerator; +import org.jfree.chart.plot.CategoryPlot; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.category.CategoryItemRenderer; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.category.DefaultCategoryDataset; +import org.jfree.experimental.chart.swt.ChartComposite; +import org.jfree.experimental.swt.SWTUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + + +/** + * Base class for our information panels. + */ +public final class HeapPanel extends BaseHeapPanel { + private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$ + + /* args to setUpdateStatus() */ + private static final int NOT_SELECTED = 0; + private static final int NOT_ENABLED = 1; + private static final int ENABLED = 2; + + /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need + * Native+1 at least. We also need 2 more entries for free area and expansion area. */ + private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; + private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; + private static final PaletteData mMapPalette = createPalette(); + + private static final boolean DISPLAY_HEAP_BITMAP = false; + private static final boolean DISPLAY_HILBERT_BITMAP = false; + + private static final int PLACEHOLDER_HILBERT_SIZE = 200; + private static final int PLACEHOLDER_LINEAR_V_SIZE = 100; + private static final int PLACEHOLDER_LINEAR_H_SIZE = 300; + + private static final int[] ZOOMS = {100, 50, 25}; + + private static final NumberFormat sByteFormatter = NumberFormat.getInstance(); + private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance(); + private static final NumberFormat sCountFormatter = NumberFormat.getInstance(); + + static { + sByteFormatter.setMinimumFractionDigits(0); + sByteFormatter.setMaximumFractionDigits(1); + sLargeByteFormatter.setMinimumFractionDigits(3); + sLargeByteFormatter.setMaximumFractionDigits(3); + + sCountFormatter.setGroupingUsed(true); + } + + private Display mDisplay; + + private Composite mTop; // real top + private Label mUpdateStatus; + private Table mHeapSummary; + private Combo mDisplayMode; + + //private ScrolledComposite mScrolledComposite; + + private Composite mDisplayBase; // base of the displays. + private StackLayout mDisplayStack; + + private Composite mStatisticsBase; + private Table mStatisticsTable; + private JFreeChart mChart; + private ChartComposite mChartComposite; + private Button mGcButton; + private DefaultCategoryDataset mAllocCountDataSet; + + private Composite mLinearBase; + private Label mLinearHeapImage; + + private Composite mHilbertBase; + private Label mHilbertHeapImage; + private Group mLegend; + private Combo mZoom; + + /** Image used for the hilbert display. Since we recreate a new image every time, we + * keep this one around to dispose it. */ + private Image mHilbertImage; + private Image mLinearImage; + private Composite[] mLayout; + + /* + * Create color palette for map. Set up titles for legend. + */ + private static PaletteData createPalette() { + RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; + colors[0] + = new RGB(192, 192, 192); // non-heap pixels are gray + mMapLegend[0] + = "(heap expansion area)"; + + colors[1] + = new RGB(0, 0, 0); // free chunks are black + mMapLegend[1] + = "free"; + + colors[HeapSegmentElement.KIND_OBJECT + 2] + = new RGB(0, 0, 255); // objects are blue + mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] + = "data object"; + + colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = new RGB(0, 255, 0); // class objects are green + mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = "class object"; + + colors[HeapSegmentElement.KIND_ARRAY_1 + 2] + = new RGB(255, 0, 0); // byte/bool arrays are red + mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] + = "1-byte array (byte[], boolean[])"; + + colors[HeapSegmentElement.KIND_ARRAY_2 + 2] + = new RGB(255, 128, 0); // short/char arrays are orange + mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] + = "2-byte array (short[], char[])"; + + colors[HeapSegmentElement.KIND_ARRAY_4 + 2] + = new RGB(255, 255, 0); // obj/int/float arrays are yellow + mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] + = "4-byte array (object[], int[], float[])"; + + colors[HeapSegmentElement.KIND_ARRAY_8 + 2] + = new RGB(255, 128, 128); // long/double arrays are pink + mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] + = "8-byte array (long[], double[])"; + + colors[HeapSegmentElement.KIND_UNKNOWN + 2] + = new RGB(255, 0, 255); // unknown objects are cyan + mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] + = "unknown object"; + + colors[HeapSegmentElement.KIND_NATIVE + 2] + = new RGB(64, 64, 64); // native objects are dark gray + mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] + = "non-Java object"; + + return new PaletteData(colors); + } + + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE || + (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) { + try { + mTop.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } catch (SWTException e) { + // display is disposed (app is quitting most likely), we do nothing. + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mTop.isDisposed()) + return; + + Client client = getCurrentClient(); + + Log.d("ddms", "HeapPanel: changed " + client); + + if (client != null) { + ClientData cd = client.getClientData(); + + if (client.isHeapUpdateEnabled()) { + mGcButton.setEnabled(true); + mDisplayMode.setEnabled(true); + setUpdateStatus(ENABLED); + } else { + setUpdateStatus(NOT_ENABLED); + mGcButton.setEnabled(false); + mDisplayMode.setEnabled(false); + } + + fillSummaryTable(cd); + + int mode = mDisplayMode.getSelectionIndex(); + if (mode == 0) { + fillDetailedTable(client, false /* forceRedraw */); + } else { + if (DISPLAY_HEAP_BITMAP) { + renderHeapData(cd, mode - 1, false /* forceRedraw */); + } + } + } else { + mGcButton.setEnabled(false); + mDisplayMode.setEnabled(false); + fillSummaryTable(null); + fillDetailedTable(null, true); + setUpdateStatus(NOT_SELECTED); + } + + // sizes of things change frequently, so redo layout + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + GridLayout gl; + + mTop = new Composite(parent, SWT.NONE); + mTop.setLayout(new GridLayout(1, false)); + mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mUpdateStatus = new Label(mTop, SWT.NONE); + setUpdateStatus(NOT_SELECTED); + + Composite summarySection = new Composite(mTop, SWT.NONE); + summarySection.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mHeapSummary = createSummaryTable(summarySection); + mGcButton = new Button(summarySection, SWT.PUSH); + mGcButton.setText("Cause GC"); + mGcButton.setEnabled(false); + mGcButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Client client = getCurrentClient(); + if (client != null) { + client.executeGarbageCollector(); + } + } + }); + + Composite comboSection = new Composite(mTop, SWT.NONE); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + comboSection.setLayout(gl); + + Label displayLabel = new Label(comboSection, SWT.NONE); + displayLabel.setText("Display: "); + + mDisplayMode = new Combo(comboSection, SWT.READ_ONLY); + mDisplayMode.setEnabled(false); + mDisplayMode.add("Stats"); + if (DISPLAY_HEAP_BITMAP) { + mDisplayMode.add("Linear"); + if (DISPLAY_HILBERT_BITMAP) { + mDisplayMode.add("Hilbert"); + } + } + + // the base of the displays. + mDisplayBase = new Composite(mTop, SWT.NONE); + mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDisplayStack = new StackLayout(); + mDisplayBase.setLayout(mDisplayStack); + + // create the statistics display + mStatisticsBase = new Composite(mDisplayBase, SWT.NONE); + //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + mStatisticsBase.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + mDisplayStack.topControl = mStatisticsBase; + + mStatisticsTable = createDetailedTable(mStatisticsBase); + mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createChart(); + + //create the linear composite + mLinearBase = new Composite(mDisplayBase, SWT.NONE); + //mLinearBase.setLayoutData(new GridData()); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + mLinearBase.setLayout(gl); + + { + mLinearHeapImage = new Label(mLinearBase, SWT.NONE); + mLinearHeapImage.setLayoutData(new GridData()); + mLinearHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay, + PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE))); + + // create a composite to contain the bottom part (legend) + Composite bottomSection = new Composite(mLinearBase, SWT.NONE); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + bottomSection.setLayout(gl); + + createLegend(bottomSection); + } + +/* + mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL); + mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + mScrolledComposite.setExpandHorizontal(true); + mScrolledComposite.setExpandVertical(true); + mScrolledComposite.setContent(mDisplayBase); +*/ + + + // create the hilbert display. + mHilbertBase = new Composite(mDisplayBase, SWT.NONE); + //mHilbertBase.setLayoutData(new GridData()); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + mHilbertBase.setLayout(gl); + + if (DISPLAY_HILBERT_BITMAP) { + mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE); + mHilbertHeapImage.setLayoutData(new GridData()); + mHilbertHeapImage.setImage(ImageHelper.createPlaceHolderArt(mDisplay, + PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE))); + + // create a composite to contain the right part (legend + zoom) + Composite rightSection = new Composite(mHilbertBase, SWT.NONE); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + rightSection.setLayout(gl); + + Composite zoomComposite = new Composite(rightSection, SWT.NONE); + gl = new GridLayout(2, false); + zoomComposite.setLayout(gl); + + Label l = new Label(zoomComposite, SWT.NONE); + l.setText("Zoom:"); + mZoom = new Combo(zoomComposite, SWT.READ_ONLY); + for (int z : ZOOMS) { + mZoom.add(String.format("%1$d%%", z)); // $NON-NLS-1$ + } + + mZoom.select(0); + mZoom.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + setLegendText(mZoom.getSelectionIndex()); + Client client = getCurrentClient(); + if (client != null) { + renderHeapData(client.getClientData(), 1, true); + mTop.pack(); + } + } + }); + + createLegend(rightSection); + } + mHilbertBase.pack(); + + mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase }; + mDisplayMode.select(0); + mDisplayMode.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = mDisplayMode.getSelectionIndex(); + Client client = getCurrentClient(); + + if (client != null) { + if (index == 0) { + fillDetailedTable(client, true /* forceRedraw */); + } else { + renderHeapData(client.getClientData(), index-1, true /* forceRedraw */); + } + } + + mDisplayStack.topControl = mLayout[index]; + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + } + }); + + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + + return mTop; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mHeapSummary.setFocus(); + } + + + private Table createSummaryTable(Composite base) { + Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); + tab.setHeaderVisible(true); + tab.setLinesVisible(true); + + TableColumn col; + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("ID"); + col.pack(); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Heap Size"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Allocated"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); // $NON-NLS-1$ + col.pack(); + col.setText("Free"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.00%"); // $NON-NLS-1$ + col.pack(); + col.setText("% Used"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000,000,000"); // $NON-NLS-1$ + col.pack(); + col.setText("# Objects"); + + return tab; + } + + private Table createDetailedTable(Composite base) { + IPreferenceStore store = DdmUiPreferences.getStore(); + + Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); + tab.setHeaderVisible(true); + tab.setLinesVisible(true); + + TableHelper.createTableColumn(tab, "Type", SWT.LEFT, + "4-byte array (object[], int[], float[])", //$NON-NLS-1$ + PREFS_STATS_COL_TYPE, store); + + TableHelper.createTableColumn(tab, "Count", SWT.RIGHT, + "00,000", //$NON-NLS-1$ + PREFS_STATS_COL_COUNT, store); + + TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_SIZE, store); + + TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_SMALLEST, store); + + TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_LARGEST, store); + + TableHelper.createTableColumn(tab, "Median", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_MEDIAN, store); + + TableHelper.createTableColumn(tab, "Average", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_AVERAGE, store); + + tab.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + + Client client = getCurrentClient(); + if (client != null) { + int index = mStatisticsTable.getSelectionIndex(); + TableItem item = mStatisticsTable.getItem(index); + + if (item != null) { + Map> heapMap = + client.getClientData().getVmHeapData().getProcessedHeapMap(); + + ArrayList list = heapMap.get(item.getData()); + if (list != null) { + showChart(list); + } + } + } + + } + }); + + return tab; + } + + /** + * Creates the chart below the statistics table + */ + private void createChart() { + mAllocCountDataSet = new DefaultCategoryDataset(); + mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet, + PlotOrientation.VERTICAL, false, true, false); + + // get the font to make a proper title. We need to convert the swt font, + // into an awt font. + Font f = mStatisticsBase.getFont(); + FontData[] fData = f.getFontData(); + + // event though on Mac OS there could be more than one fontData, we'll only use + // the first one. + FontData firstFontData = fData[0]; + + java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(), + firstFontData, true /* ensureSameSize */); + + mChart.setTitle(new TextTitle("Allocation count per size", awtFont)); + + Plot plot = mChart.getPlot(); + if (plot instanceof CategoryPlot) { + // get the plot + CategoryPlot categoryPlot = (CategoryPlot)plot; + + // set the domain axis to draw labels that are displayed even with many values. + CategoryAxis domainAxis = categoryPlot.getDomainAxis(); + domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90); + + CategoryItemRenderer renderer = categoryPlot.getRenderer(); + renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() { + public String generateToolTip(CategoryDataset dataset, int row, int column) { + // get the key for the size of the allocation + ByteLong columnKey = (ByteLong)dataset.getColumnKey(column); + String rowKey = (String)dataset.getRowKey(row); + Number value = dataset.getValue(rowKey, columnKey); + + return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey, + columnKey.getValue()); + } + }); + } + mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, // max draw width. We don't want it to zoom, so we put a big number + 3000, // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + false, // zoom + true); // tooltips + + mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + } + + private static String prettyByteCount(long bytes) { + double fracBytes = bytes; + String units = " B"; + if (fracBytes < 1024) { + return sByteFormatter.format(fracBytes) + units; + } else { + fracBytes /= 1024; + units = " KB"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = " MB"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = " GB"; + } + + return sLargeByteFormatter.format(fracBytes) + units; + } + + private static String approximateByteCount(long bytes) { + double fracBytes = bytes; + String units = ""; + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "K"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "M"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "G"; + } + + return sByteFormatter.format(fracBytes) + units; + } + + private static String addCommasToNumber(long num) { + return sCountFormatter.format(num); + } + + private static String fractionalPercent(long num, long denom) { + double val = (double)num / (double)denom; + val *= 100; + + NumberFormat nf = NumberFormat.getInstance(); + nf.setMinimumFractionDigits(2); + nf.setMaximumFractionDigits(2); + return nf.format(val) + "%"; + } + + private void fillSummaryTable(ClientData cd) { + if (mHeapSummary.isDisposed()) { + return; + } + + mHeapSummary.setRedraw(false); + mHeapSummary.removeAll(); + + if (cd != null) { + synchronized (cd) { + Iterator iter = cd.getVmHeapIds(); + + while (iter.hasNext()) { + Integer id = iter.next(); + Map heapInfo = cd.getVmHeapInfo(id); + if (heapInfo == null) { + continue; + } + long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES); + long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED); + long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED); + + TableItem item = new TableItem(mHeapSummary, SWT.NONE); + item.setText(0, id.toString()); + + item.setText(1, prettyByteCount(sizeInBytes)); + item.setText(2, prettyByteCount(bytesAllocated)); + item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated)); + item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes)); + item.setText(5, addCommasToNumber(objectsAllocated)); + } + } + } + + mHeapSummary.pack(); + mHeapSummary.setRedraw(true); + } + + private void fillDetailedTable(Client client, boolean forceRedraw) { + // first check if the client is invalid or heap updates are not enabled. + if (client == null || client.isHeapUpdateEnabled() == false) { + mStatisticsTable.removeAll(); + showChart(null); + return; + } + + ClientData cd = client.getClientData(); + + Map> heapMap; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { + // no change, we return. + return; + } + + heapMap = cd.getVmHeapData().getProcessedHeapMap(); + } + + // we have new data, lets display it. + + // First, get the current selection, and its key. + int index = mStatisticsTable.getSelectionIndex(); + Integer selectedKey = null; + if (index != -1) { + selectedKey = (Integer)mStatisticsTable.getItem(index).getData(); + } + + // disable redraws and remove all from the table. + mStatisticsTable.setRedraw(false); + mStatisticsTable.removeAll(); + + if (heapMap != null) { + int selectedIndex = -1; + ArrayList selectedList = null; + + // get the keys + Set keys = heapMap.keySet(); + int iter = 0; // use a manual iter int because Set doesn't have an index + // based accessor. + for (Integer key : keys) { + ArrayList list = heapMap.get(key); + + // check if this is the key that is supposed to be selected + if (key.equals(selectedKey)) { + selectedIndex = iter; + selectedList = list; + } + iter++; + + TableItem item = new TableItem(mStatisticsTable, SWT.NONE); + item.setData(key); + + // get the type + item.setText(0, mMapLegend[key]); + + // set the count, smallest, largest + int count = list.size(); + item.setText(1, addCommasToNumber(count)); + + if (count > 0) { + item.setText(3, prettyByteCount(list.get(0).getLength())); + item.setText(4, prettyByteCount(list.get(count-1).getLength())); + + int median = count / 2; + HeapSegmentElement element = list.get(median); + long size = element.getLength(); + item.setText(5, prettyByteCount(size)); + + long totalSize = 0; + for (int i = 0 ; i < count; i++) { + element = list.get(i); + + size = element.getLength(); + totalSize += size; + } + + // set the average and total + item.setText(2, prettyByteCount(totalSize)); + item.setText(6, prettyByteCount(totalSize / count)); + } + } + + mStatisticsTable.setRedraw(true); + + if (selectedIndex != -1) { + mStatisticsTable.setSelection(selectedIndex); + showChart(selectedList); + } else { + showChart(null); + } + } else { + mStatisticsTable.setRedraw(true); + } + } + + private static class ByteLong implements Comparable { + private long mValue; + + private ByteLong(long value) { + mValue = value; + } + + public long getValue() { + return mValue; + } + + @Override + public String toString() { + return approximateByteCount(mValue); + } + + public int compareTo(ByteLong other) { + if (mValue != other.mValue) { + return mValue < other.mValue ? -1 : 1; + } + return 0; + } + + } + + /** + * Fills the chart with the content of the list of {@link HeapSegmentElement}. + */ + private void showChart(ArrayList list) { + mAllocCountDataSet.clear(); + + if (list != null) { + String rowKey = "Alloc Count"; + + long currentSize = -1; + int currentCount = 0; + for (HeapSegmentElement element : list) { + if (element.getLength() != currentSize) { + if (currentSize != -1) { + ByteLong columnKey = new ByteLong(currentSize); + mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); + } + + currentSize = element.getLength(); + currentCount = 1; + } else { + currentCount++; + } + } + + // add the last item + if (currentSize != -1) { + ByteLong columnKey = new ByteLong(currentSize); + mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); + } + } + } + + /* + * Add a color legend to the specified table. + */ + private void createLegend(Composite parent) { + mLegend = new Group(parent, SWT.NONE); + mLegend.setText(getLegendText(0)); + + mLegend.setLayout(new GridLayout(2, false)); + + RGB[] colors = mMapPalette.colors; + + for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) { + Image tmpImage = createColorRect(parent.getDisplay(), colors[i]); + + Label l = new Label(mLegend, SWT.NONE); + l.setImage(tmpImage); + + l = new Label(mLegend, SWT.NONE); + l.setText(mMapLegend[i]); + } + } + + private String getLegendText(int level) { + int bytes = 8 * (100 / ZOOMS[level]); + + return String.format("Key (1 pixel = %1$d bytes)", bytes); + } + + private void setLegendText(int level) { + mLegend.setText(getLegendText(level)); + + } + + /* + * Create a nice rectangle in the specified color. + */ + private Image createColorRect(Display display, RGB color) { + int width = 32; + int height = 16; + + Image img = new Image(display, width, height); + GC gc = new GC(img); + gc.setBackground(new Color(display, color)); + gc.fillRectangle(0, 0, width, height); + gc.dispose(); + return img; + } + + + /* + * Are updates enabled? + */ + private void setUpdateStatus(int status) { + switch (status) { + case NOT_SELECTED: + mUpdateStatus.setText("Select a client to see heap updates"); + break; + case NOT_ENABLED: + mUpdateStatus.setText("Heap updates are " + + "NOT ENABLED for this client"); + break; + case ENABLED: + mUpdateStatus.setText("Heap updates will happen after " + + "every GC for this client"); + break; + default: + throw new RuntimeException(); + } + + mUpdateStatus.pack(); + } + + + /** + * Return the closest power of two greater than or equal to value. + * + * @param value the return value will be >= value + * @return a power of two >= value. If value > 2^31, 2^31 is returned. + */ +//xxx use Integer.highestOneBit() or numberOfLeadingZeros(). + private int nextPow2(int value) { + for (int i = 31; i >= 0; --i) { + if ((value & (1<>> 2) & 1) << 1 | + ((i >>> 4) & 1) << 2 | + ((i >>> 6) & 1) << 3 | + ((i >>> 8) & 1) << 4 | + ((i >>> 10) & 1) << 5 | + ((i >>> 12) & 1) << 6 | + ((i >>> 14) & 1) << 7 | + ((i >>> 16) & 1) << 8 | + ((i >>> 18) & 1) << 9 | + ((i >>> 20) & 1) << 10 | + ((i >>> 22) & 1) << 11 | + ((i >>> 24) & 1) << 12 | + ((i >>> 26) & 1) << 13 | + ((i >>> 28) & 1) << 14 | + ((i >>> 30) & 1) << 15; + int y = ((i >>> 1) & 1) << 0 | + ((i >>> 3) & 1) << 1 | + ((i >>> 5) & 1) << 2 | + ((i >>> 7) & 1) << 3 | + ((i >>> 9) & 1) << 4 | + ((i >>> 11) & 1) << 5 | + ((i >>> 13) & 1) << 6 | + ((i >>> 15) & 1) << 7 | + ((i >>> 17) & 1) << 8 | + ((i >>> 19) & 1) << 9 | + ((i >>> 21) & 1) << 10 | + ((i >>> 23) & 1) << 11 | + ((i >>> 25) & 1) << 12 | + ((i >>> 27) & 1) << 13 | + ((i >>> 29) & 1) << 14 | + ((i >>> 31) & 1) << 15; + try { + id.setPixel(x, y, pixData[i]); + if (x > maxX) { + maxX = x; + } + } catch (IllegalArgumentException ex) { + System.out.println("bad pixels: i " + i + + ", w " + id.width + + ", h " + id.height + + ", x " + x + + ", y " + y); + throw ex; + } + } + return maxX; + } + + private final static int HILBERT_DIR_N = 0; + private final static int HILBERT_DIR_S = 1; + private final static int HILBERT_DIR_E = 2; + private final static int HILBERT_DIR_W = 3; + + private void hilbertWalk(ImageData id, InputStream pixData, + int order, int x, int y, int dir) + throws IOException { + if (x >= id.width || y >= id.height) { + return; + } else if (order == 0) { + try { + int p = pixData.read(); + if (p >= 0) { + // flip along x=y axis; assume width == height + id.setPixel(y, x, p); + + /* Skanky; use an otherwise-unused ImageData field + * to keep track of the max x,y used. Note that x and y are inverted. + */ + if (y > id.x) { + id.x = y; + } + if (x > id.y) { + id.y = x; + } + } +//xxx just give up; don't bother walking the rest of the image + } catch (IllegalArgumentException ex) { + System.out.println("bad pixels: order " + order + + ", dir " + dir + + ", w " + id.width + + ", h " + id.height + + ", x " + x + + ", y " + y); + throw ex; + } + } else { + order--; + int delta = 1 << order; + int nextX = x + delta; + int nextY = y + delta; + + switch (dir) { + case HILBERT_DIR_E: + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_S); + break; + case HILBERT_DIR_N: + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_W); + break; + case HILBERT_DIR_S: + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_E); + break; + case HILBERT_DIR_W: + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_N); + break; + default: + throw new RuntimeException("Unexpected Hilbert direction " + + dir); + } + } + } + + private Point hilbertOrderData(ImageData id, byte pixData[]) { + + int order = 0; + for (int n = 1; n < id.width; n *= 2) { + order++; + } + /* Skanky; use an otherwise-unused ImageData field + * to keep track of maxX. + */ + Point p = new Point(0,0); + int oldIdX = id.x; + int oldIdY = id.y; + id.x = id.y = 0; + try { + hilbertWalk(id, new ByteArrayInputStream(pixData), + order, 0, 0, HILBERT_DIR_E); + p.x = id.x; + p.y = id.y; + } catch (IOException ex) { + System.err.println("Exception during hilbertWalk()"); + p.x = id.height; + p.y = id.width; + } + id.x = oldIdX; + id.y = oldIdY; + return p; + } + + private ImageData createHilbertHeapImage(byte pixData[]) { + int w, h; + + // Pick an image size that the largest of heaps will fit into. + w = (int)Math.sqrt((double)((16 * 1024 * 1024)/8)); + + // Space-filling curves require a power-of-2 width. + w = nextPow2(w); + h = w; + + // Create the heap image. + ImageData id = new ImageData(w, h, 8, mMapPalette); + + // Copy the data into the image + //int maxX = zOrderData(id, pixData); + Point maxP = hilbertOrderData(id, pixData); + + // update the max size to make it a round number once the zoom is applied + int factor = 100 / ZOOMS[mZoom.getSelectionIndex()]; + if (factor != 1) { + int tmp = maxP.x % factor; + if (tmp != 0) { + maxP.x += factor - tmp; + } + + tmp = maxP.y % factor; + if (tmp != 0) { + maxP.y += factor - tmp; + } + } + + if (maxP.y < id.height) { + // Crop the image down to the interesting part. + id = new ImageData(id.width, maxP.y, id.depth, id.palette, + id.scanlinePad, id.data); + } + + if (maxP.x < id.width) { + // crop the image again. A bit trickier this time. + ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette); + + int[] buffer = new int[maxP.x]; + for (int l = 0 ; l < id.height; l++) { + id.getPixels(0, l, maxP.x, buffer, 0); + croppedId.setPixels(0, l, maxP.x, buffer, 0); + } + + id = croppedId; + } + + // apply the zoom + if (factor != 1) { + id = id.scaledTo(id.width / factor, id.height / factor); + } + + return id; + } + + /** + * Convert the raw heap data to an image. We know we're running in + * the UI thread, so we can issue graphics commands directly. + * + * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html + * + * @param cd The client data + * @param mode The display mode. 0 = linear, 1 = hilbert. + * @param forceRedraw + */ + private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) { + Image image; + + byte[] pixData; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { + // no change, we return. + return; + } + + pixData = getSerializedData(); + } + + if (pixData != null) { + ImageData id; + if (mode == 1) { + id = createHilbertHeapImage(pixData); + } else { + id = createLinearHeapImage(pixData, 200, mMapPalette); + } + + image = new Image(mDisplay, id); + } else { + // Render a placeholder image. + int width, height; + if (mode == 1) { + width = height = PLACEHOLDER_HILBERT_SIZE; + } else { + width = PLACEHOLDER_LINEAR_H_SIZE; + height = PLACEHOLDER_LINEAR_V_SIZE; + } + image = new Image(mDisplay, width, height); + GC gc = new GC(image); + gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED)); + gc.drawLine(0, 0, width-1, height-1); + gc.dispose(); + gc = null; + } + + // set the new image + + if (mode == 1) { + if (mHilbertImage != null) { + mHilbertImage.dispose(); + } + + mHilbertImage = image; + mHilbertHeapImage.setImage(mHilbertImage); + mHilbertHeapImage.pack(true); + mHilbertBase.layout(); + mHilbertBase.pack(true); + } else { + if (mLinearImage != null) { + mLinearImage.dispose(); + } + + mLinearImage = image; + mLinearHeapImage.setImage(mLinearImage); + mLinearHeapImage.pack(true); + mLinearBase.layout(); + mLinearBase.pack(true); + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mHeapSummary); + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java new file mode 100644 index 000000000..bcbf612e5 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/IImageLoader.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2007 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; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +/** + * Interface defining an image loader. jar app/lib and plugin have different packaging method + * so each implementation will be different. + * The implementation should implement at least one of the methods, and preferably both if possible. + * + */ +public interface IImageLoader { + + /** + * Load an image from the resource from a filename + * @param filename + * @param display + */ + public Image loadImage(String filename, Display display); + + /** + * Load an ImageDescriptor from the resource from a filename + * @param filename + * @param display + */ + public ImageDescriptor loadDescriptor(String filename, Display display); + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java new file mode 100644 index 000000000..bf425d944 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ITableFocusListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2007 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; + +import org.eclipse.swt.dnd.Clipboard; + +/** + * An object listening to focus change in Table objects.
+ * For application not relying on a RCP to provide menu changes based on focus, + * this class allows to get monitor the focus change of several Table widget + * and update the menu action accordingly. + */ +public interface ITableFocusListener { + + public interface IFocusedTableActivator { + public void copy(Clipboard clipboard); + + public void selectAll(); + } + + public void focusGained(IFocusedTableActivator activator); + + public void focusLost(IFocusedTableActivator activator); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java new file mode 100644 index 000000000..d65978b46 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageHelper.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class ImageHelper { + + /** + * Loads an image from a resource. This method used a class to locate the + * resources, and then load the filename from /images inside the resources.
+ * Extra parameters allows for creation of a replacement image of the + * loading failed. + * + * @param loader the image loader used. + * @param display the Display object + * @param fileName the file name + * @param width optional width to create replacement Image. If -1, null be + * be returned if the loading fails. + * @param height optional height to create replacement Image. If -1, null be + * be returned if the loading fails. + * @param phColor optional color to create replacement Image. If null, Blue + * color will be used. + * @return a new Image or null if the loading failed and the optional + * replacement size was -1 + */ + public static Image loadImage(IImageLoader loader, Display display, + String fileName, int width, int height, Color phColor) { + + Image img = null; + if (loader != null) { + img = loader.loadImage(fileName, display); + } + + if (img == null) { + Log.w("ddms", "Couldn't load " + fileName); + // if we had the extra parameter to create replacement image then we + // create and return it. + if (width != -1 && height != -1) { + return createPlaceHolderArt(display, width, height, + phColor != null ? phColor : display + .getSystemColor(SWT.COLOR_BLUE)); + } + + // otherwise, just return null + return null; + } + + return img; + } + + /** + * Create place-holder art with the specified color. + */ + public static Image createPlaceHolderArt(Display display, int width, + int height, Color color) { + Image img = new Image(display, width, height); + GC gc = new GC(img); + gc.setForeground(color); + gc.drawLine(0, 0, width, height); + gc.drawLine(0, height - 1, width, -1); + gc.dispose(); + return img; + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java new file mode 100644 index 000000000..76f228533 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ImageLoader.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2007 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; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.io.InputStream; + +/** + * Image loader for an normal standalone app. + */ +public class ImageLoader implements IImageLoader { + + /** class used as reference to get the reources */ + private Class mClass; + + /** + * Creates a loader for a specific class. The class allows java to figure + * out which .jar file to search for the image. + * + * @param theClass + */ + public ImageLoader(Class theClass) { + mClass = theClass; + } + + public ImageDescriptor loadDescriptor(String filename, Display display) { + // we don't support ImageDescriptor + return null; + } + + public Image loadImage(String filename, Display display) { + + String tmp = "/images/" + filename; + InputStream imageStream = mClass.getResourceAsStream(tmp); + + if (imageStream != null) { + Image img = new Image(display, imageStream); + if (img == null) + throw new NullPointerException("couldn't load " + tmp); + return img; + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java new file mode 100644 index 000000000..ed402c0dc --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/InfoPanel.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +/** + * Display client info in a two-column format. + */ +public class InfoPanel extends TablePanel { + private Table mTable; + private TableColumn mCol2; + + private static final String mLabels[] = { + "DDM-aware?", + "App description:", + "VM version:", + "Process ID:", + "Supports Profiling Control:", + "Supports HPROF Control:", + }; + private static final int ENT_DDM_AWARE = 0; + private static final int ENT_APP_DESCR = 1; + private static final int ENT_VM_VERSION = 2; + private static final int ENT_PROCESS_ID = 3; + private static final int ENT_SUPPORTS_PROFILING = 4; + private static final int ENT_SUPPORTS_HPROF = 5; + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mTable.setHeaderVisible(false); + mTable.setLinesVisible(false); + + TableColumn col1 = new TableColumn(mTable, SWT.RIGHT); + col1.setText("name"); + mCol2 = new TableColumn(mTable, SWT.LEFT); + mCol2.setText("PlaceHolderContentForWidth"); + + TableItem item; + for (int i = 0; i < mLabels.length; i++) { + item = new TableItem(mTable, SWT.NONE); + item.setText(0, mLabels[i]); + item.setText(1, "-"); + } + + col1.pack(); + mCol2.pack(); + + return mTable; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTable.setFocus(); + } + + + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_PORT}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_INFO) == Client.CHANGE_INFO) { + if (mTable.isDisposed()) + return; + + mTable.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } + } + } + + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()} + */ + @Override + public void clientSelected() { + if (mTable.isDisposed()) + return; + + Client client = getCurrentClient(); + + if (client == null) { + for (int i = 0; i < mLabels.length; i++) { + TableItem item = mTable.getItem(i); + item.setText(1, "-"); + } + } else { + TableItem item; + String clientDescription, vmIdentifier, isDdmAware, + pid; + + ClientData cd = client.getClientData(); + synchronized (cd) { + clientDescription = (cd.getClientDescription() != null) ? + cd.getClientDescription() : "?"; + vmIdentifier = (cd.getVmIdentifier() != null) ? + cd.getVmIdentifier() : "?"; + isDdmAware = cd.isDdmAware() ? + "yes" : "no"; + pid = (cd.getPid() != 0) ? + String.valueOf(cd.getPid()) : "?"; + } + + item = mTable.getItem(ENT_APP_DESCR); + item.setText(1, clientDescription); + item = mTable.getItem(ENT_VM_VERSION); + item.setText(1, vmIdentifier); + item = mTable.getItem(ENT_DDM_AWARE); + item.setText(1, isDdmAware); + item = mTable.getItem(ENT_PROCESS_ID); + item.setText(1, pid); + + item = mTable.getItem(ENT_SUPPORTS_PROFILING); + if (cd.hasFeature(ClientData.FEATURE_PROFILING_STREAMING)) { + item.setText(1, "Yes"); + } else if (cd.hasFeature(ClientData.FEATURE_PROFILING)) { + item.setText(1, "Yes (Application must be able to write on the SD Card)"); + } else { + item.setText(1, "No"); + } + + item = mTable.getItem(ENT_SUPPORTS_HPROF); + if (cd.hasFeature(ClientData.FEATURE_HPROF_STREAMING)) { + item.setText(1, "Yes"); + } else if (cd.hasFeature(ClientData.FEATURE_HPROF)) { + item.setText(1, "Yes (Application must be able to write on the SD Card)"); + } else { + item.setText(1, "No"); + } + } + + mCol2.pack(); + + //Log.i("ddms", "InfoPanel: changed " + client); + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mTable); + } +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java new file mode 100644 index 000000000..0b2460ba4 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java @@ -0,0 +1,1633 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.Log; +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeLibraryMapInfo; +import com.android.ddmlib.NativeStackCallInfo; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; +import com.android.ddmuilib.annotation.WorkerThread; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; + +/** + * Panel with native heap information. + */ +public final class NativeHeapPanel extends BaseHeapPanel { + + /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need + * Native+1 at least. We also need 2 more entries for free area and expansion area. */ + private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; + private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; + private static final PaletteData mMapPalette = createPalette(); + + private static final int ALLOC_DISPLAY_ALL = 0; + private static final int ALLOC_DISPLAY_PRE_ZYGOTE = 1; + private static final int ALLOC_DISPLAY_POST_ZYGOTE = 2; + + private Display mDisplay; + + private Composite mBase; + + private Label mUpdateStatus; + + /** combo giving choice of what to display: all, pre-zygote, post-zygote */ + private Combo mAllocDisplayCombo; + + private Button mFullUpdateButton; + + // see CreateControl() + //private Button mDiffUpdateButton; + + private Combo mDisplayModeCombo; + + /** stack composite for mode (1-2) & 3 */ + private Composite mTopStackComposite; + + private StackLayout mTopStackLayout; + + /** stack composite for mode 1 & 2 */ + private Composite mAllocationStackComposite; + + private StackLayout mAllocationStackLayout; + + /** top level container for mode 1 & 2 */ + private Composite mTableModeControl; + + /** top level object for the allocation mode */ + private Control mAllocationModeTop; + + /** top level for the library mode */ + private Control mLibraryModeTopControl; + + /** composite for page UI and total memory display */ + private Composite mPageUIComposite; + + private Label mTotalMemoryLabel; + + private Label mPageLabel; + + private Button mPageNextButton; + + private Button mPagePreviousButton; + + private Table mAllocationTable; + + private Table mLibraryTable; + + private Table mLibraryAllocationTable; + + private Table mDetailTable; + + private Label mImage; + + private int mAllocDisplayMode = ALLOC_DISPLAY_ALL; + + /** + * pointer to current stackcall thread computation in order to quit it if + * required (new update requested) + */ + private StackCallThread mStackCallThread; + + /** Current Library Allocation table fill thread. killed if selection changes */ + private FillTableThread mFillTableThread; + + /** + * current client data. Used to access the malloc info when switching pages + * or selecting allocation to show stack call + */ + private ClientData mClientData; + + /** + * client data from a previous display. used when asking for an "update & diff" + */ + private ClientData mBackUpClientData; + + /** list of NativeAllocationInfo objects filled with the list from ClientData */ + private final ArrayList mAllocations = + new ArrayList(); + + /** list of the {@link NativeAllocationInfo} being displayed based on the selection + * of {@link #mAllocDisplayCombo}. + */ + private final ArrayList mDisplayedAllocations = + new ArrayList(); + + /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */ + private final ArrayList mBackUpAllocations = + new ArrayList(); + + /** back up of the total memory, used when doing an "update & diff" */ + private int mBackUpTotalMemory; + + private int mCurrentPage = 0; + + private int mPageCount = 0; + + /** + * list of allocation per Library. This is created from the list of + * NativeAllocationInfo objects that is stored in the ClientData object. Since we + * don't keep this list around, it is recomputed everytime the client + * changes. + */ + private final ArrayList mLibraryAllocations = + new ArrayList(); + + /* args to setUpdateStatus() */ + private static final int NOT_SELECTED = 0; + + private static final int NOT_ENABLED = 1; + + private static final int ENABLED = 2; + + private static final int DISPLAY_PER_PAGE = 20; + + private static final String PREFS_ALLOCATION_SASH = "NHallocSash"; //$NON-NLS-1$ + private static final String PREFS_LIBRARY_SASH = "NHlibrarySash"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_ADDRESS = "NHdetailAddress"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_LIBRARY = "NHdetailLibrary"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_METHOD = "NHdetailMethod"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_FILE = "NHdetailFile"; //$NON-NLS-1$ + private static final String PREFS_DETAIL_LINE = "NHdetailLine"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_TOTAL = "NHallocTotal"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_COUNT = "NHallocCount"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_SIZE = "NHallocSize"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_LIBRARY = "NHallocLib"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_METHOD = "NHallocMethod"; //$NON-NLS-1$ + private static final String PREFS_ALLOC_FILE = "NHallocFile"; //$NON-NLS-1$ + private static final String PREFS_LIB_LIBRARY = "NHlibLibrary"; //$NON-NLS-1$ + private static final String PREFS_LIB_SIZE = "NHlibSize"; //$NON-NLS-1$ + private static final String PREFS_LIB_COUNT = "NHlibCount"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_TOTAL = "NHlibAllocTotal"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_COUNT = "NHlibAllocCount"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_SIZE = "NHlibAllocSize"; //$NON-NLS-1$ + private static final String PREFS_LIBALLOC_METHOD = "NHlibAllocMethod"; //$NON-NLS-1$ + + /** static formatter object to format all numbers as #,### */ + private static DecimalFormat sFormatter; + static { + sFormatter = (DecimalFormat)NumberFormat.getInstance(); + if (sFormatter != null) + sFormatter = new DecimalFormat("#,###"); + else + sFormatter.applyPattern("#,###"); + } + + + /** + * caching mechanism to avoid recomputing the backtrace for a particular + * address several times. + */ + private HashMap mSourceCache = + new HashMap(); + private long mTotalSize; + private Button mSaveButton; + private Button mSymbolsButton; + + /** + * thread class to convert the address call into method, file and line + * number in the background. + */ + private class StackCallThread extends BackgroundThread { + private ClientData mClientData; + + public StackCallThread(ClientData cd) { + mClientData = cd; + } + + public ClientData getClientData() { + return mClientData; + } + + @Override + public void run() { + // loop through all the NativeAllocationInfo and init them + Iterator iter = mAllocations.iterator(); + int total = mAllocations.size(); + int count = 0; + while (iter.hasNext()) { + + if (isQuitting()) + return; + + NativeAllocationInfo info = iter.next(); + if (info.isStackCallResolved() == false) { + final Long[] list = info.getStackCallAddresses(); + final int size = list.length; + + ArrayList resolvedStackCall = + new ArrayList(); + + for (int i = 0; i < size; i++) { + long addr = list[i]; + + // first check if the addr has already been converted. + NativeStackCallInfo source = mSourceCache.get(addr); + + // if not we convert it + if (source == null) { + source = sourceForAddr(addr); + mSourceCache.put(addr, source); + } + + resolvedStackCall.add(source); + } + + info.setResolvedStackCall(resolvedStackCall); + } + // after every DISPLAY_PER_PAGE we ask for a ui refresh, unless + // we reach total, since we also do it after the loop + // (only an issue in case we have a perfect number of page) + count++; + if ((count % DISPLAY_PER_PAGE) == 0 && count != total) { + if (updateNHAllocationStackCalls(mClientData, count) == false) { + // looks like the app is quitting, so we just + // stopped the thread + return; + } + } + } + + updateNHAllocationStackCalls(mClientData, count); + } + + private NativeStackCallInfo sourceForAddr(long addr) { + NativeLibraryMapInfo library = getLibraryFor(addr); + + if (library != null) { + + Addr2Line process = Addr2Line.getProcess(library.getLibraryName()); + if (process != null) { + // remove the base of the library address + long value = addr - library.getStartAddress(); + NativeStackCallInfo info = process.getAddress(value); + if (info != null) { + return info; + } + } + } + + return new NativeStackCallInfo(library != null ? library.getLibraryName() : null, + Long.toHexString(addr), ""); + } + + private NativeLibraryMapInfo getLibraryFor(long addr) { + Iterator it = mClientData.getNativeLibraryMapInfo(); + + while (it.hasNext()) { + NativeLibraryMapInfo info = it.next(); + + if (info.isWithinLibrary(addr)) { + return info; + } + } + + Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr)); + return null; + } + + /** + * update the Native Heap panel with the amount of allocation for which the + * stack call has been computed. This is called from a non UI thread, but + * will be executed in the UI thread. + * + * @param count the amount of allocation + * @return false if the display was disposed and the update couldn't happen + */ + private boolean updateNHAllocationStackCalls(final ClientData clientData, final int count) { + if (mDisplay.isDisposed() == false) { + mDisplay.asyncExec(new Runnable() { + public void run() { + updateAllocationStackCalls(clientData, count); + } + }); + return true; + } + return false; + } + } + + private class FillTableThread extends BackgroundThread { + private LibraryAllocations mLibAlloc; + + private int mMax; + + public FillTableThread(LibraryAllocations liballoc, int m) { + mLibAlloc = liballoc; + mMax = m; + } + + @Override + public void run() { + for (int i = mMax; i > 0 && isQuitting() == false; i -= 10) { + updateNHLibraryAllocationTable(mLibAlloc, mMax - i, mMax - i + 10); + } + } + + /** + * updates the library allocation table in the Native Heap panel. This is + * called from a non UI thread, but will be executed in the UI thread. + * + * @param liballoc the current library allocation object being displayed + * @param start start index of items that need to be displayed + * @param end end index of the items that need to be displayed + */ + private void updateNHLibraryAllocationTable(final LibraryAllocations libAlloc, + final int start, final int end) { + if (mDisplay.isDisposed() == false) { + mDisplay.asyncExec(new Runnable() { + public void run() { + updateLibraryAllocationTable(libAlloc, start, end); + } + }); + } + + } + } + + /** class to aggregate allocations per library */ + public static class LibraryAllocations { + private String mLibrary; + + private final ArrayList mLibAllocations = + new ArrayList(); + + private int mSize; + + private int mCount; + + /** construct the aggregate object for a library */ + public LibraryAllocations(final String lib) { + mLibrary = lib; + } + + /** get the library name */ + public String getLibrary() { + return mLibrary; + } + + /** add a NativeAllocationInfo object to this aggregate object */ + public void addAllocation(NativeAllocationInfo info) { + mLibAllocations.add(info); + } + + /** get an iterator on the NativeAllocationInfo objects */ + public Iterator getAllocations() { + return mLibAllocations.iterator(); + } + + /** get a NativeAllocationInfo object by index */ + public NativeAllocationInfo getAllocation(int index) { + return mLibAllocations.get(index); + } + + /** returns the NativeAllocationInfo object count */ + public int getAllocationSize() { + return mLibAllocations.size(); + } + + /** returns the total allocation size */ + public int getSize() { + return mSize; + } + + /** returns the number of allocations */ + public int getCount() { + return mCount; + } + + /** + * compute the allocation count and size for allocation objects added + * through addAllocation(), and sort the objects by + * total allocation size. + */ + public void computeAllocationSizeAndCount() { + mSize = 0; + mCount = 0; + for (NativeAllocationInfo info : mLibAllocations) { + mCount += info.getAllocationCount(); + mSize += info.getAllocationCount() * info.getSize(); + } + Collections.sort(mLibAllocations, new Comparator() { + public int compare(NativeAllocationInfo o1, NativeAllocationInfo o2) { + return o2.getAllocationCount() * o2.getSize() - + o1.getAllocationCount() * o1.getSize(); + } + }); + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + + mDisplay = parent.getDisplay(); + + mBase = new Composite(parent, SWT.NONE); + GridLayout gl = new GridLayout(1, false); + gl.horizontalSpacing = 0; + gl.verticalSpacing = 0; + mBase.setLayout(gl); + mBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // composite for + Composite tmp = new Composite(mBase, SWT.NONE); + tmp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + tmp.setLayout(gl = new GridLayout(2, false)); + gl.marginWidth = gl.marginHeight = 0; + + mFullUpdateButton = new Button(tmp, SWT.NONE); + mFullUpdateButton.setText("Full Update"); + mFullUpdateButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mBackUpClientData = null; + mDisplayModeCombo.setEnabled(false); + mSaveButton.setEnabled(false); + emptyTables(); + // if we already have a stack call computation for this + // client + // we stop it + if (mStackCallThread != null && + mStackCallThread.getClientData() == mClientData) { + mStackCallThread.quit(); + mStackCallThread = null; + } + mLibraryAllocations.clear(); + Client client = getCurrentClient(); + if (client != null) { + client.requestNativeHeapInformation(); + } + } + }); + + mUpdateStatus = new Label(tmp, SWT.NONE); + mUpdateStatus.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // top layout for the combos and oter controls on the right. + Composite top_layout = new Composite(mBase, SWT.NONE); + top_layout.setLayout(gl = new GridLayout(4, false)); + gl.marginWidth = gl.marginHeight = 0; + + new Label(top_layout, SWT.NONE).setText("Show:"); + + mAllocDisplayCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY); + mAllocDisplayCombo.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mAllocDisplayCombo.add("All Allocations"); + mAllocDisplayCombo.add("Pre-Zygote Allocations"); + mAllocDisplayCombo.add("Zygote Child Allocations (Z)"); + mAllocDisplayCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onAllocDisplayChange(); + } + }); + mAllocDisplayCombo.select(0); + + // separator + Label separator = new Label(top_layout, SWT.SEPARATOR | SWT.VERTICAL); + GridData gd; + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + gd.verticalSpan = 2; + + mSaveButton = new Button(top_layout, SWT.PUSH); + mSaveButton.setText("Save..."); + mSaveButton.setEnabled(false); + mSaveButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(mBase.getShell(), SWT.SAVE); + + fileDialog.setText("Save Allocations"); + fileDialog.setFileName("allocations.txt"); + + String fileName = fileDialog.open(); + if (fileName != null) { + saveAllocations(fileName); + } + } + }); + + /* + * TODO: either fix the diff mechanism or remove it altogether. + mDiffUpdateButton = new Button(top_layout, SWT.NONE); + mDiffUpdateButton.setText("Update && Diff"); + mDiffUpdateButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // since this is an update and diff, we need to store the + // current list + // of mallocs + mBackUpAllocations.clear(); + mBackUpAllocations.addAll(mAllocations); + mBackUpClientData = mClientData; + mBackUpTotalMemory = mClientData.getTotalNativeMemory(); + + mDisplayModeCombo.setEnabled(false); + emptyTables(); + // if we already have a stack call computation for this + // client + // we stop it + if (mStackCallThread != null && + mStackCallThread.getClientData() == mClientData) { + mStackCallThread.quit(); + mStackCallThread = null; + } + mLibraryAllocations.clear(); + Client client = getCurrentClient(); + if (client != null) { + client.requestNativeHeapInformation(); + } + } + }); + */ + + Label l = new Label(top_layout, SWT.NONE); + l.setText("Display:"); + + mDisplayModeCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY); + mDisplayModeCombo.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mDisplayModeCombo.setItems(new String[] { "Allocation List", "By Libraries" }); + mDisplayModeCombo.select(0); + mDisplayModeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + switchDisplayMode(); + } + }); + mDisplayModeCombo.setEnabled(false); + + mSymbolsButton = new Button(top_layout, SWT.PUSH); + mSymbolsButton.setText("Load Symbols"); + mSymbolsButton.setEnabled(false); + + + // create a composite that will contains the actual content composites, + // in stack mode layout. + // This top level composite contains 2 other composites. + // * one for both Allocations and Libraries mode + // * one for flat mode (which is gone for now) + + mTopStackComposite = new Composite(mBase, SWT.NONE); + mTopStackComposite.setLayout(mTopStackLayout = new StackLayout()); + mTopStackComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // create 1st and 2nd modes + createTableDisplay(mTopStackComposite); + + mTopStackLayout.topControl = mTableModeControl; + mTopStackComposite.layout(); + + setUpdateStatus(NOT_SELECTED); + + // Work in progress + // TODO add image display of native heap. + //mImage = new Label(mBase, SWT.NONE); + + mBase.pack(); + + return mBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + // TODO + } + + + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) { + if (mBase.isDisposed()) + return; + + mBase.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mBase.isDisposed()) + return; + + Client client = getCurrentClient(); + + mDisplayModeCombo.setEnabled(false); + emptyTables(); + + Log.d("ddms", "NativeHeapPanel: changed " + client); + + if (client != null) { + ClientData cd = client.getClientData(); + mClientData = cd; + + // if (cd.getShowHeapUpdates()) + setUpdateStatus(ENABLED); + // else + // setUpdateStatus(NOT_ENABLED); + + initAllocationDisplay(); + + //renderBitmap(cd); + } else { + mClientData = null; + setUpdateStatus(NOT_SELECTED); + } + + mBase.pack(); + } + + /** + * Update the UI with the newly compute stack calls, unless the UI switched + * to a different client. + * + * @param cd the ClientData for which the stack call are being computed. + * @param count the current count of allocations for which the stack calls + * have been computed. + */ + @WorkerThread + public void updateAllocationStackCalls(ClientData cd, int count) { + // we have to check that the panel still shows the same clientdata than + // the thread is computing for. + if (cd == mClientData) { + + int total = mAllocations.size(); + + if (count == total) { + // we're done: do something + mDisplayModeCombo.setEnabled(true); + mSaveButton.setEnabled(true); + + mStackCallThread = null; + } else { + // work in progress, update the progress bar. +// mUiThread.setStatusLine("Computing stack call: " + count +// + "/" + total); + } + + // FIXME: attempt to only update when needed. + // Because the number of pages is not related to mAllocations.size() anymore + // due to pre-zygote/post-zygote display, update all the time. + // At some point we should remove the pages anyway, since it's getting computed + // really fast now. +// if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count +// || (count == total && mCurrentPage == mPageCount - 1)) { + try { + // get the current selection of the allocation + int index = mAllocationTable.getSelectionIndex(); + NativeAllocationInfo info = null; + + if (index != -1) { + info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData(); + } + + // empty the table + emptyTables(); + + // fill it again + fillAllocationTable(); + + // reselect + mAllocationTable.setSelection(index); + + // display detail table if needed + if (info != null) { + fillDetailTable(info); + } + } catch (SWTException e) { + if (mAllocationTable.isDisposed()) { + // looks like the table is disposed. Let's ignore it. + } else { + throw e; + } + } + + } else { + // old client still running. doesn't really matter. + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mAllocationTable); + addTableToFocusListener(mLibraryTable); + addTableToFocusListener(mLibraryAllocationTable); + addTableToFocusListener(mDetailTable); + } + + protected void onAllocDisplayChange() { + mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex(); + + // create the new list + updateAllocDisplayList(); + + updateTotalMemoryDisplay(); + + // reset the ui. + mCurrentPage = 0; + updatePageUI(); + switchDisplayMode(); + } + + private void updateAllocDisplayList() { + mTotalSize = 0; + mDisplayedAllocations.clear(); + for (NativeAllocationInfo info : mAllocations) { + if (mAllocDisplayMode == ALLOC_DISPLAY_ALL || + (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) { + mDisplayedAllocations.add(info); + mTotalSize += info.getSize() * info.getAllocationCount(); + } else { + // skip this item + continue; + } + } + + int count = mDisplayedAllocations.size(); + + mPageCount = count / DISPLAY_PER_PAGE; + + // need to add a page for the rest of the div + if ((count % DISPLAY_PER_PAGE) > 0) { + mPageCount++; + } + } + + private void updateTotalMemoryDisplay() { + switch (mAllocDisplayMode) { + case ALLOC_DISPLAY_ALL: + mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + case ALLOC_DISPLAY_PRE_ZYGOTE: + mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + case ALLOC_DISPLAY_POST_ZYGOTE: + mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + } + } + + + private void switchDisplayMode() { + switch (mDisplayModeCombo.getSelectionIndex()) { + case 0: {// allocations + mTopStackLayout.topControl = mTableModeControl; + mAllocationStackLayout.topControl = mAllocationModeTop; + mAllocationStackComposite.layout(); + mTopStackComposite.layout(); + emptyTables(); + fillAllocationTable(); + } + break; + case 1: {// libraries + mTopStackLayout.topControl = mTableModeControl; + mAllocationStackLayout.topControl = mLibraryModeTopControl; + mAllocationStackComposite.layout(); + mTopStackComposite.layout(); + emptyTables(); + fillLibraryTable(); + } + break; + } + } + + private void initAllocationDisplay() { + mAllocations.clear(); + mAllocations.addAll(mClientData.getNativeAllocationList()); + + updateAllocDisplayList(); + + // if we have a previous clientdata and it matches the current one. we + // do a diff between the new list and the old one. + if (mBackUpClientData != null && mBackUpClientData == mClientData) { + + ArrayList add = new ArrayList(); + + // we go through the list of NativeAllocationInfo in the new list and check if + // there's one with the same exact data (size, allocation, count and + // stackcall addresses) in the old list. + // if we don't find any, we add it to the "add" list + for (NativeAllocationInfo mi : mAllocations) { + boolean found = false; + for (NativeAllocationInfo old_mi : mBackUpAllocations) { + if (mi.equals(old_mi)) { + found = true; + break; + } + } + if (found == false) { + add.add(mi); + } + } + + // put the result in mAllocations + mAllocations.clear(); + mAllocations.addAll(add); + + // display the difference in memory usage. This is computed + // calculating the memory usage of the objects in mAllocations. + int count = 0; + for (NativeAllocationInfo allocInfo : mAllocations) { + count += allocInfo.getSize() * allocInfo.getAllocationCount(); + } + + mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes", + sFormatter.format(count))); + } + else { + // display the full memory usage + updateTotalMemoryDisplay(); + //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0); + } + mTotalMemoryLabel.pack(); + + // update the page ui + mDisplayModeCombo.select(0); + + mLibraryAllocations.clear(); + + // reset to first page + mCurrentPage = 0; + + // update the label + updatePageUI(); + + // now fill the allocation Table with the current page + switchDisplayMode(); + + // start the thread to compute the stack calls + if (mAllocations.size() > 0) { + mStackCallThread = new StackCallThread(mClientData); + mStackCallThread.start(); + } + } + + private void updatePageUI() { + + // set the label and pack to update the layout, otherwise + // the label will be cut off if the new size is bigger + if (mPageCount == 0) { + mPageLabel.setText("0 of 0 allocations."); + } else { + StringBuffer buffer = new StringBuffer(); + // get our starting index + int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1; + // end index, taking into account the last page can be half full + int count = mDisplayedAllocations.size(); + int end = Math.min(start + DISPLAY_PER_PAGE - 1, count); + buffer.append(sFormatter.format(start)); + buffer.append(" - "); + buffer.append(sFormatter.format(end)); + buffer.append(" of "); + buffer.append(sFormatter.format(count)); + buffer.append(" allocations."); + mPageLabel.setText(buffer.toString()); + } + + // handle the button enabled state. + mPagePreviousButton.setEnabled(mCurrentPage > 0); + // reminder: mCurrentPage starts at 0. + mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1); + + mPageLabel.pack(); + mPageUIComposite.pack(); + + } + + private void fillAllocationTable() { + // get the count + int count = mDisplayedAllocations.size(); + + // get our starting index + int start = mCurrentPage * DISPLAY_PER_PAGE; + + // loop for DISPLAY_PER_PAGE or till we reach count + int end = start + DISPLAY_PER_PAGE; + + for (int i = start; i < end && i < count; i++) { + NativeAllocationInfo info = mDisplayedAllocations.get(i); + + TableItem item = null; + + if (mAllocDisplayMode == ALLOC_DISPLAY_ALL) { + item = new TableItem(mAllocationTable, SWT.NONE); + item.setText(0, (info.isZygoteChild() ? "Z " : "") + + sFormatter.format(info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) { + item = new TableItem(mAllocationTable, SWT.NONE); + item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + } else { + // skip this item + continue; + } + + item.setData(info); + + NativeStackCallInfo bti = info.getRelevantStackCallInfo(); + if (bti != null) { + String lib = bti.getLibraryName(); + String method = bti.getMethodName(); + String source = bti.getSourceFile(); + if (lib != null) + item.setText(3, lib); + if (method != null) + item.setText(4, method); + if (source != null) + item.setText(5, source); + } + } + } + + private void fillLibraryTable() { + // fill the library table + sortAllocationsPerLibrary(); + + for (LibraryAllocations liballoc : mLibraryAllocations) { + if (liballoc != null) { + TableItem item = new TableItem(mLibraryTable, SWT.NONE); + String lib = liballoc.getLibrary(); + item.setText(0, lib != null ? lib : ""); + item.setText(1, sFormatter.format(liballoc.getSize())); + item.setText(2, sFormatter.format(liballoc.getCount())); + } + } + } + + private void fillLibraryAllocationTable() { + mLibraryAllocationTable.removeAll(); + mDetailTable.removeAll(); + int index = mLibraryTable.getSelectionIndex(); + if (index != -1) { + LibraryAllocations liballoc = mLibraryAllocations.get(index); + // start a thread that will fill table 10 at a time to keep the ui + // responsive, but first we kill the previous one if there was one + if (mFillTableThread != null) { + mFillTableThread.quit(); + } + mFillTableThread = new FillTableThread(liballoc, + liballoc.getAllocationSize()); + mFillTableThread.start(); + } + } + + public void updateLibraryAllocationTable(LibraryAllocations liballoc, + int start, int end) { + try { + if (mLibraryTable.isDisposed() == false) { + int index = mLibraryTable.getSelectionIndex(); + if (index != -1) { + LibraryAllocations newliballoc = mLibraryAllocations.get( + index); + if (newliballoc == liballoc) { + int count = liballoc.getAllocationSize(); + for (int i = start; i < end && i < count; i++) { + NativeAllocationInfo info = liballoc.getAllocation(i); + + TableItem item = new TableItem( + mLibraryAllocationTable, SWT.NONE); + item.setText(0, sFormatter.format( + info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + + NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo(); + if (stackCallInfo != null) { + item.setText(3, stackCallInfo.getMethodName()); + } + } + } else { + // we should quit the thread + if (mFillTableThread != null) { + mFillTableThread.quit(); + mFillTableThread = null; + } + } + } + } + } catch (SWTException e) { + Log.e("ddms", "error when updating the library allocation table"); + } + } + + private void fillDetailTable(final NativeAllocationInfo mi) { + mDetailTable.removeAll(); + mDetailTable.setRedraw(false); + + try { + // populate the detail Table with the back trace + Long[] addresses = mi.getStackCallAddresses(); + NativeStackCallInfo[] resolvedStackCall = mi.getResolvedStackCall(); + + if (resolvedStackCall == null) { + return; + } + + for (int i = 0 ; i < resolvedStackCall.length ; i++) { + if (addresses[i] == null || addresses[i].longValue() == 0) { + continue; + } + + long addr = addresses[i].longValue(); + NativeStackCallInfo source = resolvedStackCall[i]; + + TableItem item = new TableItem(mDetailTable, SWT.NONE); + item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$ + + String libraryName = source.getLibraryName(); + String methodName = source.getMethodName(); + String sourceFile = source.getSourceFile(); + int lineNumber = source.getLineNumber(); + + if (libraryName != null) + item.setText(1, libraryName); + if (methodName != null) + item.setText(2, methodName); + if (sourceFile != null) + item.setText(3, sourceFile); + if (lineNumber != -1) + item.setText(4, Integer.toString(lineNumber)); + } + } finally { + mDetailTable.setRedraw(true); + } + } + + /* + * Are updates enabled? + */ + private void setUpdateStatus(int status) { + switch (status) { + case NOT_SELECTED: + mUpdateStatus.setText("Select a client to see heap info"); + mAllocDisplayCombo.setEnabled(false); + mFullUpdateButton.setEnabled(false); + //mDiffUpdateButton.setEnabled(false); + break; + case NOT_ENABLED: + mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client"); + mAllocDisplayCombo.setEnabled(false); + mFullUpdateButton.setEnabled(false); + //mDiffUpdateButton.setEnabled(false); + break; + case ENABLED: + mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data"); + mAllocDisplayCombo.setEnabled(true); + mFullUpdateButton.setEnabled(true); + //mDiffUpdateButton.setEnabled(true); + break; + default: + throw new RuntimeException(); + } + + mUpdateStatus.pack(); + } + + /** + * Create the Table display. This includes a "detail" Table in the bottom + * half and 2 modes in the top half: allocation Table and + * library+allocations Tables. + * + * @param base the top parent to create the display into + */ + private void createTableDisplay(Composite base) { + final int minPanelWidth = 60; + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + // top level composite for mode 1 & 2 + mTableModeControl = new Composite(base, SWT.NONE); + GridLayout gl = new GridLayout(1, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + mTableModeControl.setLayout(gl); + mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE); + mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTotalMemoryLabel.setText("Total Memory: 0 Bytes"); + + // the top half of these modes is dynamic + + final Composite sash_composite = new Composite(mTableModeControl, + SWT.NONE); + sash_composite.setLayout(new FormLayout()); + sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // create the stacked composite + mAllocationStackComposite = new Composite(sash_composite, SWT.NONE); + mAllocationStackLayout = new StackLayout(); + mAllocationStackComposite.setLayout(mAllocationStackLayout); + mAllocationStackComposite.setLayoutData(new GridData( + GridData.FILL_BOTH)); + + // create the top half for mode 1 + createAllocationTopHalf(mAllocationStackComposite); + + // create the top half for mode 2 + createLibraryTopHalf(mAllocationStackComposite); + + final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL); + + // bottom half of these modes is the same: detail table + createDetailTable(sash_composite); + + // init value for stack + mAllocationStackLayout.topControl = mAllocationModeTop; + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(mTotalMemoryLabel, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mAllocationStackComposite.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) { + sashData.top = new FormAttachment(0, + prefs.getInt(PREFS_ALLOCATION_SASH)); + } else { + sashData.top = new FormAttachment(50, 0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mDetailTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = sash_composite.getClientArea(); + int bottom = panelRect.height - sashRect.height - minPanelWidth; + e.y = Math.max(Math.min(e.y, bottom), minPanelWidth); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + prefs.setValue(PREFS_ALLOCATION_SASH, e.y); + sash_composite.layout(); + } + } + }); + } + + private void createDetailTable(Composite base) { + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); + mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDetailTable.setHeaderVisible(true); + mDetailTable.setLinesVisible(true); + + TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT, + "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT, + "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$ + } + + private void createAllocationTopHalf(Composite b) { + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + Composite base = new Composite(b, SWT.NONE); + mAllocationModeTop = base; + GridLayout gl = new GridLayout(1, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + gl.verticalSpacing = 0; + base.setLayout(gl); + base.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // horizontal layout for memory total and pages UI + mPageUIComposite = new Composite(base, SWT.NONE); + mPageUIComposite.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_BEGINNING)); + gl = new GridLayout(3, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + gl.horizontalSpacing = 0; + mPageUIComposite.setLayout(gl); + + // Page UI + mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE); + mPagePreviousButton.setText("<"); + mPagePreviousButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mCurrentPage--; + updatePageUI(); + emptyTables(); + fillAllocationTable(); + } + }); + + mPageNextButton = new Button(mPageUIComposite, SWT.NONE); + mPageNextButton.setText(">"); + mPageNextButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mCurrentPage++; + updatePageUI(); + emptyTables(); + fillAllocationTable(); + } + }); + + mPageLabel = new Label(mPageUIComposite, SWT.NONE); + mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + updatePageUI(); + + mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); + mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mAllocationTable.setHeaderVisible(true); + mAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT, + "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT, + "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT, + "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$ + + mAllocationTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection index + int index = mAllocationTable.getSelectionIndex(); + TableItem item = mAllocationTable.getItem(index); + if (item != null && item.getData() instanceof NativeAllocationInfo) { + fillDetailTable((NativeAllocationInfo)item.getData()); + } + } + }); + } + + private void createLibraryTopHalf(Composite base) { + final int minPanelWidth = 60; + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + // create a composite that'll contain 2 tables horizontally + final Composite top = new Composite(base, SWT.NONE); + mLibraryModeTopControl = top; + top.setLayout(new FormLayout()); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // first table: library + mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mLibraryTable.setHeaderVisible(true); + mLibraryTable.setLinesVisible(true); + + TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT, + "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT, + "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$ + + mLibraryTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + fillLibraryAllocationTable(); + } + }); + + final Sash sash = new Sash(top, SWT.VERTICAL); + + // 2nd table: allocation per library + mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mLibraryAllocationTable.setHeaderVisible(true); + mLibraryAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn(mLibraryAllocationTable, "Total", + SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Count", + SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Size", + SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Method", + SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$ + + mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the index of the selection in the library table + int index1 = mLibraryTable.getSelectionIndex(); + // get the index in the library allocation table + int index2 = mLibraryAllocationTable.getSelectionIndex(); + // get the MallocInfo object + LibraryAllocations liballoc = mLibraryAllocations.get(index1); + NativeAllocationInfo info = liballoc.getAllocation(index2); + fillDetailTable(info); + } + }); + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(sash, 0); + mLibraryTable.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) { + sashData.left = new FormAttachment(0, + prefs.getInt(PREFS_LIBRARY_SASH)); + } else { + sashData.left = new FormAttachment(50, 0); + } + sashData.bottom = new FormAttachment(100, 0); + sashData.top = new FormAttachment(0, 0); // 50% across + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(sash, 0); + data.right = new FormAttachment(100, 0); + mLibraryAllocationTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = top.getClientArea(); + int right = panelRect.width - sashRect.width - minPanelWidth; + e.x = Math.max(Math.min(e.x, right), minPanelWidth); + if (e.x != sashRect.x) { + sashData.left = new FormAttachment(0, e.x); + prefs.setValue(PREFS_LIBRARY_SASH, e.y); + top.layout(); + } + } + }); + } + + private void emptyTables() { + mAllocationTable.removeAll(); + mLibraryTable.removeAll(); + mLibraryAllocationTable.removeAll(); + mDetailTable.removeAll(); + } + + private void sortAllocationsPerLibrary() { + if (mClientData != null) { + mLibraryAllocations.clear(); + + // create a hash map of LibraryAllocations to access aggregate + // objects already created + HashMap libcache = + new HashMap(); + + // get the allocation count + int count = mDisplayedAllocations.size(); + for (int i = 0; i < count; i++) { + NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i); + + NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo(); + if (stackCallInfo != null) { + String libraryName = stackCallInfo.getLibraryName(); + LibraryAllocations liballoc = libcache.get(libraryName); + if (liballoc == null) { + // didn't find a library allocation object already + // created so we create one + liballoc = new LibraryAllocations(libraryName); + // add it to the cache + libcache.put(libraryName, liballoc); + // add it to the list + mLibraryAllocations.add(liballoc); + } + // add the MallocInfo object to it. + liballoc.addAllocation(allocInfo); + } + } + // now that the list is created, we need to compute the size and + // sort it by size. This will also sort the MallocInfo objects + // inside each LibraryAllocation objects. + for (LibraryAllocations liballoc : mLibraryAllocations) { + liballoc.computeAllocationSizeAndCount(); + } + + // now we sort it + Collections.sort(mLibraryAllocations, + new Comparator() { + public int compare(LibraryAllocations o1, + LibraryAllocations o2) { + return o2.getSize() - o1.getSize(); + } + }); + } + } + + private void renderBitmap(ClientData cd) { + byte[] pixData; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false) { + // no change, we return. + return; + } + + pixData = getSerializedData(); + + ImageData id = createLinearHeapImage(pixData, 200, mMapPalette); + Image image = new Image(mBase.getDisplay(), id); + mImage.setImage(image); + mImage.pack(true); + } + } + + /* + * Create color palette for map. Set up titles for legend. + */ + private static PaletteData createPalette() { + RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; + colors[0] + = new RGB(192, 192, 192); // non-heap pixels are gray + mMapLegend[0] + = "(heap expansion area)"; + + colors[1] + = new RGB(0, 0, 0); // free chunks are black + mMapLegend[1] + = "free"; + + colors[HeapSegmentElement.KIND_OBJECT + 2] + = new RGB(0, 0, 255); // objects are blue + mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] + = "data object"; + + colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = new RGB(0, 255, 0); // class objects are green + mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = "class object"; + + colors[HeapSegmentElement.KIND_ARRAY_1 + 2] + = new RGB(255, 0, 0); // byte/bool arrays are red + mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] + = "1-byte array (byte[], boolean[])"; + + colors[HeapSegmentElement.KIND_ARRAY_2 + 2] + = new RGB(255, 128, 0); // short/char arrays are orange + mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] + = "2-byte array (short[], char[])"; + + colors[HeapSegmentElement.KIND_ARRAY_4 + 2] + = new RGB(255, 255, 0); // obj/int/float arrays are yellow + mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] + = "4-byte array (object[], int[], float[])"; + + colors[HeapSegmentElement.KIND_ARRAY_8 + 2] + = new RGB(255, 128, 128); // long/double arrays are pink + mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] + = "8-byte array (long[], double[])"; + + colors[HeapSegmentElement.KIND_UNKNOWN + 2] + = new RGB(255, 0, 255); // unknown objects are cyan + mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] + = "unknown object"; + + colors[HeapSegmentElement.KIND_NATIVE + 2] + = new RGB(64, 64, 64); // native objects are dark gray + mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] + = "non-Java object"; + + return new PaletteData(colors); + } + + private void saveAllocations(String fileName) { + try { + PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); + + for (NativeAllocationInfo alloc : mAllocations) { + out.println(alloc.toString()); + } + out.close(); + } catch (IOException e) { + Log.e("Native", e); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java new file mode 100644 index 000000000..d910cc74d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/Panel.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2007 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; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + + +/** + * Base class for our information panels. + */ +public abstract class Panel { + + public final Control createPanel(Composite parent) { + Control panelControl = createControl(parent); + + postCreation(); + + return panelControl; + } + + protected abstract void postCreation(); + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + protected abstract Control createControl(Composite parent); + + /** + * Sets the focus to the proper control inside the panel. + */ + public abstract void setFocus(); +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java new file mode 100644 index 000000000..533372e8c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/PortFieldEditor.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 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; + +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.swt.widgets.Composite; + +/** + * Edit an integer field, validating it as a port number. + */ +public class PortFieldEditor extends IntegerFieldEditor { + + public boolean mRecursiveCheck = false; + + public PortFieldEditor(String name, String label, Composite parent) { + super(name, label, parent); + setValidateStrategy(VALIDATE_ON_KEY_STROKE); + } + + /* + * Get the current value of the field, as an integer. + */ + public int getCurrentValue() { + int val; + try { + val = Integer.parseInt(getStringValue()); + } + catch (NumberFormatException nfe) { + val = -1; + } + return val; + } + + /* + * Check the validity of the field. + */ + @Override + protected boolean checkState() { + if (super.checkState() == false) { + return false; + } + //Log.i("ddms", "check state " + getStringValue()); + boolean err = false; + int val = getCurrentValue(); + if (val < 1024 || val > 32767) { + setErrorMessage("Port must be between 1024 and 32767"); + err = true; + } else { + setErrorMessage(null); + err = false; + } + showErrorMessage(); + return !err; + } + + protected void updateCheckState(PortFieldEditor pfe) { + pfe.refreshValidState(); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java new file mode 100644 index 000000000..3a108f624 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ScreenShotDialog.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.RawImage; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.ImageTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.io.IOException; + + +/** + * Gather a screen shot from the device and save it to a file. + */ +public class ScreenShotDialog extends Dialog { + + private Label mBusyLabel; + private Label mImageLabel; + private Button mSave; + private IDevice mDevice; + private RawImage mRawImage; + private Clipboard mClipboard; + + + /** + * Create with default style. + */ + public ScreenShotDialog(Shell parent) { + this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + mClipboard = new Clipboard(parent.getDisplay()); + } + + /** + * Create with app-defined style. + */ + public ScreenShotDialog(Shell parent, int style) { + super(parent, style); + } + + /** + * Prepare and display the dialog. + * @param device The {@link IDevice} from which to get the screenshot. + */ + public void open(IDevice device) { + mDevice = device; + + Shell parent = getParent(); + Shell shell = new Shell(parent, getStyle()); + shell.setText("Device Screen Capture"); + + createContents(shell); + shell.pack(); + shell.open(); + + updateDeviceImage(shell); + + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + } + + /* + * Create the screen capture dialog contents. + */ + private void createContents(final Shell shell) { + GridData data; + + final int colCount = 5; + + shell.setLayout(new GridLayout(colCount, true)); + + // "refresh" button + Button refresh = new Button(shell, SWT.PUSH); + refresh.setText("Refresh"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + refresh.setLayoutData(data); + refresh.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateDeviceImage(shell); + } + }); + + // "rotate" button + Button rotate = new Button(shell, SWT.PUSH); + rotate.setText("Rotate"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + rotate.setLayoutData(data); + rotate.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mRawImage != null) { + mRawImage = mRawImage.getRotated(); + updateImageDisplay(shell); + } + } + }); + + // "save" button + mSave = new Button(shell, SWT.PUSH); + mSave.setText("Save"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + mSave.setLayoutData(data); + mSave.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + saveImage(shell); + } + }); + + Button copy = new Button(shell, SWT.PUSH); + copy.setText("Copy"); + copy.setToolTipText("Copy the screenshot to the clipboard"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + copy.setLayoutData(data); + copy.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + copy(); + } + }); + + + // "done" button + Button done = new Button(shell, SWT.PUSH); + done.setText("Done"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + done.setLayoutData(data); + done.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + shell.close(); + } + }); + + // title/"capturing" label + mBusyLabel = new Label(shell, SWT.NONE); + mBusyLabel.setText("Preparing..."); + data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); + data.horizontalSpan = colCount; + mBusyLabel.setLayoutData(data); + + // space for the image + mImageLabel = new Label(shell, SWT.BORDER); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.horizontalSpan = colCount; + mImageLabel.setLayoutData(data); + Display display = shell.getDisplay(); + mImageLabel.setImage(ImageHelper.createPlaceHolderArt( + display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE))); + + + shell.setDefaultButton(done); + } + + /** + * Copies the content of {@link #mImageLabel} to the clipboard. + */ + private void copy() { + mClipboard.setContents( + new Object[] { + mImageLabel.getImage().getImageData() + }, new Transfer[] { + ImageTransfer.getInstance() + }); + } + + /** + * Captures a new image from the device, and display it. + */ + private void updateDeviceImage(Shell shell) { + mBusyLabel.setText("Capturing..."); // no effect + + shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT)); + + mRawImage = getDeviceImage(); + + updateImageDisplay(shell); + } + + /** + * Updates the display with {@link #mRawImage}. + * @param shell + */ + private void updateImageDisplay(Shell shell) { + Image image; + if (mRawImage == null) { + Display display = shell.getDisplay(); + image = ImageHelper.createPlaceHolderArt( + display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE)); + + mSave.setEnabled(false); + mBusyLabel.setText("Screen not available"); + } else { + // convert raw data to an Image. + PaletteData palette = new PaletteData( + mRawImage.getRedMask(), + mRawImage.getGreenMask(), + mRawImage.getBlueMask()); + + ImageData imageData = new ImageData(mRawImage.width, mRawImage.height, + mRawImage.bpp, palette, 1, mRawImage.data); + image = new Image(getParent().getDisplay(), imageData); + + mSave.setEnabled(true); + mBusyLabel.setText("Captured image:"); + } + + mImageLabel.setImage(image); + mImageLabel.pack(); + shell.pack(); + + // there's no way to restore old cursor; assume it's ARROW + shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_ARROW)); + } + + /** + * Grabs an image from an ADB-connected device and returns it as a {@link RawImage}. + */ + private RawImage getDeviceImage() { + try { + return mDevice.getScreenshot(); + } + catch (IOException ioe) { + Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage()); + return null; + } + } + + /* + * Prompt the user to save the image to disk. + */ + private void saveImage(Shell shell) { + FileDialog dlg = new FileDialog(shell, SWT.SAVE); + String fileName; + + dlg.setText("Save image..."); + dlg.setFileName("device.png"); + dlg.setFilterPath(DdmUiPreferences.getStore().getString("lastImageSaveDir")); + dlg.setFilterNames(new String[] { + "PNG Files (*.png)" + }); + dlg.setFilterExtensions(new String[] { + "*.png" //$NON-NLS-1$ + }); + + fileName = dlg.open(); + if (fileName != null) { + DdmUiPreferences.getStore().setValue("lastImageSaveDir", dlg.getFilterPath()); + + Log.d("ddms", "Saving image to " + fileName); + ImageData imageData = mImageLabel.getImage().getImageData(); + + try { + ImageLoader loader = new ImageLoader(); + loader.data = new ImageData[] { imageData }; + loader.save(fileName, SWT.IMAGE_PNG); + } + catch (SWTException e) { + Log.w("ddms", "Unable to save " + fileName + ": " + e.getMessage()); + } + } + } + +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java new file mode 100644 index 000000000..e6d2211c3 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SelectionDependentPanel.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; + +/** + * A Panel that requires {@link Device}/{@link Client} selection notifications. + */ +public abstract class SelectionDependentPanel extends Panel { + private IDevice mCurrentDevice = null; + private Client mCurrentClient = null; + + /** + * Returns the current {@link Device}. + * @return the current device or null if none are selected. + */ + protected final IDevice getCurrentDevice() { + return mCurrentDevice; + } + + /** + * Returns the current {@link Client}. + * @return the current client or null if none are selected. + */ + protected final Client getCurrentClient() { + return mCurrentClient; + } + + /** + * Sent when a new device is selected. + * @param selectedDevice the selected device. + */ + public final void deviceSelected(IDevice selectedDevice) { + if (selectedDevice != mCurrentDevice) { + mCurrentDevice = selectedDevice; + deviceSelected(); + } + } + + /** + * Sent when a new client is selected. + * @param selectedClient the selected client. + */ + public final void clientSelected(Client selectedClient) { + if (selectedClient != mCurrentClient) { + mCurrentClient = selectedClient; + clientSelected(); + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + public abstract void deviceSelected(); + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + public abstract void clientSelected(); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java new file mode 100644 index 000000000..3358962c9 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/StackTracePanel.java @@ -0,0 +1,258 @@ +/* + * 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; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IStackTraceInfo; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Table; + +/** + * Stack Trace Panel. + *

This is not a panel in the regular sense. Instead this is just an object around the creation + * and management of a Stack Trace display. + *

UI creation is done through + * {@link #createPanel(Composite, String, String, String, String, String, IPreferenceStore)}. + * + */ +public final class StackTracePanel { + + private static ISourceRevealer sSourceRevealer; + + private Table mStackTraceTable; + private TableViewer mStackTraceViewer; + + private Client mCurrentClient; + + + /** + * Content Provider to display the stack trace of a thread. + * Expected input is a {@link IStackTraceInfo} object. + */ + private static class StackTraceContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof IStackTraceInfo) { + // getElement cannot return null, so we return an empty array + // if there's no stack trace + StackTraceElement trace[] = ((IStackTraceInfo)inputElement).getStackTrace(); + if (trace != null) { + return trace; + } + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + + /** + * A Label Provider to use with {@link StackTraceContentProvider}. It expects the elements to be + * of type {@link StackTraceElement}. + */ + private static class StackTraceLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof StackTraceElement) { + StackTraceElement traceElement = (StackTraceElement)element; + switch (columnIndex) { + case 0: + return traceElement.getClassName(); + case 1: + return traceElement.getMethodName(); + case 2: + return traceElement.getFileName(); + case 3: + return Integer.toString(traceElement.getLineNumber()); + case 4: + return Boolean.toString(traceElement.isNativeMethod()); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Classes which implement this interface provide a method that is able to reveal a method + * in a source editor + */ + public interface ISourceRevealer { + /** + * Sent to reveal a particular line in a source editor + * @param applicationName the name of the application running the source. + * @param className the fully qualified class name + * @param line the line to reveal + */ + public void reveal(String applicationName, String className, int line); + } + + + /** + * Sets the {@link ISourceRevealer} object able to reveal source code in a source editor. + * @param revealer + */ + public static void setSourceRevealer(ISourceRevealer revealer) { + sSourceRevealer = revealer; + } + + /** + * Creates the controls for the StrackTrace display. + *

This method will set the parent {@link Composite} to use a {@link GridLayout} with + * 2 columns. + * @param parent the parent composite. + * @param prefs_stack_col_class + * @param prefs_stack_col_method + * @param prefs_stack_col_file + * @param prefs_stack_col_line + * @param prefs_stack_col_native + * @param store + */ + public Table createPanel(Composite parent, String prefs_stack_col_class, + String prefs_stack_col_method, String prefs_stack_col_file, String prefs_stack_col_line, + String prefs_stack_col_native, IPreferenceStore store) { + + mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mStackTraceTable.setHeaderVisible(true); + mStackTraceTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mStackTraceTable, + "Class", + SWT.LEFT, + "SomeLongClassName", //$NON-NLS-1$ + prefs_stack_col_class, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Method", + SWT.LEFT, + "someLongMethod", //$NON-NLS-1$ + prefs_stack_col_method, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "File", + SWT.LEFT, + "android/somepackage/someotherpackage/somefile.class", //$NON-NLS-1$ + prefs_stack_col_file, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Line", + SWT.RIGHT, + "99999", //$NON-NLS-1$ + prefs_stack_col_line, store); + + TableHelper.createTableColumn( + mStackTraceTable, + "Native", + SWT.LEFT, + "Native", //$NON-NLS-1$ + prefs_stack_col_native, store); + + mStackTraceViewer = new TableViewer(mStackTraceTable); + mStackTraceViewer.setContentProvider(new StackTraceContentProvider()); + mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider()); + + mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + if (sSourceRevealer != null && mCurrentClient != null) { + // get the selected stack trace element + ISelection selection = mStackTraceViewer.getSelection(); + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof StackTraceElement) { + StackTraceElement traceElement = (StackTraceElement)object; + + if (traceElement.isNativeMethod() == false) { + sSourceRevealer.reveal( + mCurrentClient.getClientData().getClientDescription(), + traceElement.getClassName(), + traceElement.getLineNumber()); + } + } + } + } + } + }); + + return mStackTraceTable; + } + + /** + * Sets the input for the {@link TableViewer}. + * @param input the {@link IStackTraceInfo} that will provide the viewer with the list of + * {@link StackTraceElement} + */ + public void setViewerInput(IStackTraceInfo input) { + mStackTraceViewer.setInput(input); + mStackTraceViewer.refresh(); + } + + /** + * Sets the current client running the stack trace. + * @param currentClient the {@link Client}. + */ + public void setCurrentClient(Client currentClient) { + mCurrentClient = currentClient; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SyncProgressMonitor.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SyncProgressMonitor.java new file mode 100644 index 000000000..59259849c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SyncProgressMonitor.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2009 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; + +import com.android.ddmlib.SyncService.ISyncProgressMonitor; + +import org.eclipse.core.runtime.IProgressMonitor; + +/** + * Implementation of the {@link ISyncProgressMonitor} wrapping an Eclipse {@link IProgressMonitor}. + */ +public class SyncProgressMonitor implements ISyncProgressMonitor { + + private IProgressMonitor mMonitor; + private String mName; + + public SyncProgressMonitor(IProgressMonitor monitor, String name) { + mMonitor = monitor; + mName = name; + } + + public void start(int totalWork) { + mMonitor.beginTask(mName, totalWork); + } + + public void stop() { + mMonitor.done(); + } + + public void advance(int work) { + mMonitor.worked(work); + } + + public boolean isCanceled() { + return mMonitor.isCanceled(); + } + + public void startSubTask(String name) { + mMonitor.subTask(name); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java new file mode 100644 index 000000000..8ef237cc7 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/SysinfoPanel.java @@ -0,0 +1,582 @@ +/* + * 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; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.data.general.DefaultPieDataset; +import org.jfree.experimental.chart.swt.ChartComposite; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Displays system information graphs obtained from a bugreport file or device. + */ +public class SysinfoPanel extends TablePanel implements IShellOutputReceiver { + + // UI components + private Label mLabel; + private Button mFetchButton; + private Combo mDisplayMode; + + private DefaultPieDataset mDataset; + + // The bugreport file to process + private File mDataFile; + + // To get output from adb commands + private FileOutputStream mTempStream; + + // Selects the current display: MODE_CPU, etc. + private int mMode = 0; + + private static final int MODE_CPU = 0; + private static final int MODE_ALARM = 1; + private static final int MODE_WAKELOCK = 2; + private static final int MODE_MEMINFO = 3; + private static final int MODE_SYNC = 4; + + // argument to dumpsys; section in the bugreport holding the data + private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm", + "batteryinfo", "MEMORY INFO", "content"}; + + private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo", + "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank", + "dumpsys content"}; + + private static final String CAPTIONS[] = {"CPU load", "Alarms", + "Wakelocks", "Memory usage", "Sync"}; + + /** + * Generates the dataset to display. + * + * @param file The bugreport file to process. + */ + public void generateDataset(File file) { + mDataset.clear(); + mLabel.setText(""); + if (file == null) { + return; + } + try { + BufferedReader br = getBugreportReader(file); + if (mMode == MODE_CPU) { + readCpuDataset(br); + } else if (mMode == MODE_ALARM) { + readAlarmDataset(br); + } else if (mMode == MODE_WAKELOCK) { + readWakelockDataset(br); + } else if (mMode == MODE_MEMINFO) { + readMeminfoDataset(br); + } else if (mMode == MODE_SYNC) { + readSyncDataset(br); + } + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Sent when a new device is selected. The new device can be accessed with + * {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + if (getCurrentDevice() != null) { + mFetchButton.setEnabled(true); + loadFromDevice(); + } else { + mFetchButton.setEnabled(false); + } + } + + /** + * Sent when a new client is selected. The new client can be accessed with + * {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mDisplayMode.setFocus(); + } + + /** + * Fetches a new bugreport from the device and updates the display. + * Fetching is asynchronous. See also addOutput, flush, and isCancelled. + */ + private void loadFromDevice() { + try { + initShellOutputBuffer(); + if (mMode == MODE_MEMINFO) { + // Hack to add bugreport-style section header for meminfo + mTempStream.write("------ MEMORY INFO ------\n".getBytes()); + } + getCurrentDevice().executeShellCommand( + DUMP_COMMAND[mMode], this); + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Initializes temporary output file for executeShellCommand(). + * + * @throws IOException on file error + */ + void initShellOutputBuffer() throws IOException { + mDataFile = File.createTempFile("ddmsfile", ".txt"); + mDataFile.deleteOnExit(); + mTempStream = new FileOutputStream(mDataFile); + } + + /** + * Adds output to the temp file. IShellOutputReceiver method. Called by + * executeShellCommand(). + */ + public void addOutput(byte[] data, int offset, int length) { + try { + mTempStream.write(data, offset, length); + } + catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Processes output from shell command. IShellOutputReceiver method. The + * output is passed to generateDataset(). Called by executeShellCommand() on + * completion. + */ + public void flush() { + if (mTempStream != null) { + try { + mTempStream.close(); + generateDataset(mDataFile); + mTempStream = null; + mDataFile = null; + } catch (IOException e) { + Log.e("DDMS", e); + } + } + } + + /** + * IShellOutputReceiver method. + * + * @return false - don't cancel + */ + public boolean isCancelled() { + return false; + } + + /** + * Create our controls for the UI panel. + */ + @Override + protected Control createControl(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, false)); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Composite buttons = new Composite(top, SWT.NONE); + buttons.setLayout(new RowLayout()); + + mDisplayMode = new Combo(buttons, SWT.PUSH); + for (String mode : CAPTIONS) { + mDisplayMode.add(mode); + } + mDisplayMode.select(mMode); + mDisplayMode.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mMode = mDisplayMode.getSelectionIndex(); + if (mDataFile != null) { + generateDataset(mDataFile); + } else if (getCurrentDevice() != null) { + loadFromDevice(); + } + } + }); + + final Button loadButton = new Button(buttons, SWT.PUSH); + loadButton.setText("Load from File"); + loadButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(loadButton.getShell(), + SWT.OPEN); + fileDialog.setText("Load bugreport"); + String filename = fileDialog.open(); + if (filename != null) { + mDataFile = new File(filename); + generateDataset(mDataFile); + } + } + }); + + mFetchButton = new Button(buttons, SWT.PUSH); + mFetchButton.setText("Update from Device"); + mFetchButton.setEnabled(false); + mFetchButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + loadFromDevice(); + } + }); + + mLabel = new Label(top, SWT.NONE); + mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mDataset = new DefaultPieDataset(); + JFreeChart chart = ChartFactory.createPieChart("", mDataset, false + /* legend */, true/* tooltips */, false /* urls */); + + ChartComposite chartComposite = new ChartComposite(top, + SWT.BORDER, chart, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, + // max draw width. We don't want it to zoom, so we put a big number + 3000, + // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + false, // zoom + true); + chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + return top; + } + + public void clientChanged(final Client client, int changeMask) { + // Don't care + } + + /** + * Helper to open a bugreport and skip to the specified section. + * + * @param file File to open + * @return Reader to bugreport file + * @throws java.io.IOException on file error + */ + private BufferedReader getBugreportReader(File file) throws + IOException { + BufferedReader br = new BufferedReader(new FileReader(file)); + // Skip over the unwanted bugreport sections + while (true) { + String line = br.readLine(); + if (line == null) { + Log.d("DDMS", "Service not found " + line); + break; + } + if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) && + line.indexOf(BUGREPORT_SECTION[mMode]) > 0) { + break; + } + } + return br; + } + + /** + * Parse the time string generated by BatteryStats. + * A typical new-format string is "11d 13h 45m 39s 999ms". + * A typical old-format string is "12.3 sec". + * @return time in ms + */ + private static long parseTimeMs(String s) { + long total = 0; + // Matches a single component e.g. "12.3 sec" or "45ms" + Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)"); + Matcher m = p.matcher(s); + while (m.find()) { + String label = m.group(2); + if ("sec".equals(label)) { + // Backwards compatibility with old time format + total += (long) (Double.parseDouble(m.group(1)) * 1000); + continue; + } + long value = Integer.parseInt(m.group(1)); + if ("d".equals(label)) { + total += value * 24 * 60 * 60 * 1000; + } else if ("h".equals(label)) { + total += value * 60 * 60 * 1000; + } else if ("m".equals(label)) { + total += value * 60 * 1000; + } else if ("s".equals(label)) { + total += value * 1000; + } else if ("ms".equals(label)) { + total += value; + } + } + return total; + } + /** + * Processes wakelock information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readWakelockDataset(BufferedReader br) throws IOException { + Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial"); + Pattern totalPattern = Pattern.compile("Total: (.+) uptime"); + double total = 0; + boolean inCurrent = false; + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith("Current Battery Usage Statistics")) { + inCurrent = true; + } else if (inCurrent) { + Matcher m = lockPattern.matcher(line); + if (m.find()) { + double value = parseTimeMs(m.group(2)) / 1000.; + mDataset.setValue(m.group(1), value); + total -= value; + } else { + m = totalPattern.matcher(line); + if (m.find()) { + total += parseTimeMs(m.group(1)) / 1000.; + } + } + } + } + if (total > 0) { + mDataset.setValue("Unlocked", total); + } + } + + /** + * Processes alarm information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readAlarmDataset(BufferedReader br) throws IOException { + Pattern pattern = Pattern + .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags"); + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + Matcher m = pattern.matcher(line); + if (m.find()) { + long count = Long.parseLong(m.group(1)); + String name = m.group(2); + mDataset.setValue(name, count); + } + } + } + + /** + * Processes cpu load information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readCpuDataset(BufferedReader br) throws IOException { + Pattern pattern = Pattern + .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel"); + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith("Load:")) { + mLabel.setText(line); + continue; + } + Matcher m = pattern.matcher(line); + if (m.find()) { + String name = m.group(1); + long both = Long.parseLong(m.group(2)); + long user = Long.parseLong(m.group(3)); + long kernel = Long.parseLong(m.group(4)); + if ("TOTAL".equals(name)) { + if (both < 100) { + mDataset.setValue("Idle", (100 - both)); + } + } else { + // Try to make graphs more useful even with rounding; + // log often has 0% user + 0% kernel = 1% total + // We arbitrarily give extra to kernel + if (user > 0) { + mDataset.setValue(name + " (user)", user); + } + if (kernel > 0) { + mDataset.setValue(name + " (kernel)" , both - user); + } + if (user == 0 && kernel == 0 && both > 0) { + mDataset.setValue(name, both); + } + } + } + } + } + + /** + * Processes meminfo information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readMeminfoDataset(BufferedReader br) throws IOException { + Pattern valuePattern = Pattern.compile("(\\d+) kB"); + long total = 0; + long other = 0; + mLabel.setText("PSS in kB"); + + // Scan meminfo + while (true) { + String line = br.readLine(); + if (line == null) { + // End of file + break; + } + Matcher m = valuePattern.matcher(line); + if (m.find()) { + long kb = Long.parseLong(m.group(1)); + if (line.startsWith("MemTotal")) { + total = kb; + } else if (line.startsWith("MemFree")) { + mDataset.setValue("Free", kb); + total -= kb; + } else if (line.startsWith("Slab")) { + mDataset.setValue("Slab", kb); + total -= kb; + } else if (line.startsWith("PageTables")) { + mDataset.setValue("PageTables", kb); + total -= kb; + } else if (line.startsWith("Buffers") && kb > 0) { + mDataset.setValue("Buffers", kb); + total -= kb; + } else if (line.startsWith("Inactive")) { + mDataset.setValue("Inactive", kb); + total -= kb; + } else if (line.startsWith("MemFree")) { + mDataset.setValue("Free", kb); + total -= kb; + } + } else { + break; + } + } + // Scan procrank + while (true) { + String line = br.readLine(); + if (line == null) { + break; + } + if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) { + // procrank header + continue; + } + if (line.indexOf("----") >= 0) { + //end of procrank section + break; + } + // Extract pss field from procrank output + long pss = Long.parseLong(line.substring(23, 31).trim()); + String cmdline = line.substring(43).trim().replace("/system/bin/", ""); + // Arbitrary minimum size to display + if (pss > 2000) { + mDataset.setValue(cmdline, pss); + } else { + other += pss; + } + total -= pss; + } + mDataset.setValue("Other", other); + mDataset.setValue("Unknown", total); + } + + /** + * Processes sync information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + void readSyncDataset(BufferedReader br) throws IOException { + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith(" |") && line.length() > 70) { + String authority = line.substring(3, 18).trim(); + String duration = line.substring(61, 70).trim(); + // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime) + String durParts[] = duration.split(":"); + if (durParts.length == 2) { + long dur = Long.parseLong(durParts[0]) * 60 + Long + .parseLong(durParts[1]); + mDataset.setValue(authority, dur); + } else if (duration.length() == 3) { + long dur = Long.parseLong(durParts[0]) * 3600 + + Long.parseLong(durParts[1]) * 60 + Long + .parseLong(durParts[2]); + mDataset.setValue(authority, dur); + } + } + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java new file mode 100644 index 000000000..9d557e04a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TableHelper.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2007 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; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +/** + * Utility class to help using Table objects. + * + */ +public final class TableHelper { + /** + * Create a TableColumn with the specified parameters. If a + * PreferenceStore object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + * @return The TableColumn object that was created + */ + public static TableColumn createTableColumn(Table parent, String header, + int style, String sample_text, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TableColumn col = new TableColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TableColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + + return col; + } + + /** + * Create a TreeColumn with the specified parameters. If a + * PreferenceStore object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + */ + public static void createTreeColumn(Tree parent, String header, int style, + String sample_text, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TreeColumn col = new TreeColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TreeColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + } + + /** + * Create a TreeColumn with the specified parameters. If a + * PreferenceStore object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param width the width of the column if the preference value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + */ + public static void createTreeColumn(Tree parent, String header, int style, + int width, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TreeColumn col = new TreeColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setWidth(width); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, width); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TreeColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java new file mode 100644 index 000000000..b037193ca --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/TablePanel.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; + +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.util.Arrays; + +/** + * Base class for panel containing Table that need to support copy-paste-selectAll + */ +public abstract class TablePanel extends ClientDisplayPanel { + private ITableFocusListener mGlobalListener; + + /** + * Sets a TableFocusListener which will be notified when one of the tables + * gets or loses focus. + * + * @param listener + */ + public final void setTableFocusListener(ITableFocusListener listener) { + // record the global listener, to make sure table created after + // this call will still be setup. + mGlobalListener = listener; + + setTableFocusListener(); + } + + /** + * Sets up the Table of object of the panel to work with the global listener.
+ * Default implementation does nothing. + */ + protected void setTableFocusListener() { + + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus. + * + * @param table the Table object. + * @param colStart + * @param colEnd + */ + protected final void addTableToFocusListener(final Table table, + final int colStart, final int colEnd) { + // create the activator for this table + final IFocusedTableActivator activator = new IFocusedTableActivator() { + public void copy(Clipboard clipboard) { + int[] selection = table.getSelectionIndices(); + + // we need to sort the items to be sure. + Arrays.sort(selection); + + // all lines must be concatenated. + StringBuilder sb = new StringBuilder(); + + // loop on the selection and output the file. + for (int i : selection) { + TableItem item = table.getItem(i); + for (int c = colStart ; c <= colEnd ; c++) { + sb.append(item.getText(c)); + sb.append('\t'); + } + sb.append('\n'); + } + + // now add that to the clipboard if the string has content + String data = sb.toString(); + if (data != null || data.length() > 0) { + clipboard.setContents( + new Object[] { data }, + new Transfer[] { TextTransfer.getInstance() }); + } + } + + public void selectAll() { + table.selectAll(); + } + }; + + // add the focus listener on the table to notify the global listener + table.addFocusListener(new FocusListener() { + public void focusGained(FocusEvent e) { + mGlobalListener.focusGained(activator); + } + + public void focusLost(FocusEvent e) { + mGlobalListener.focusLost(activator); + } + }); + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus.
+ * When the copy method is invoked, all columns are put in the clipboard, separated + * by tabs + * + * @param table the Table object. + */ + protected final void addTableToFocusListener(final Table table) { + addTableToFocusListener(table, 0, table.getColumnCount()-1); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java new file mode 100644 index 000000000..d94d4f340 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/ThreadPanel.java @@ -0,0 +1,572 @@ +/* + * Copyright (C) 2007 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; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ThreadInfo; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Table; + +import java.util.Date; + +/** + * Base class for our information panels. + */ +public class ThreadPanel extends TablePanel { + + private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$ + private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$ + + private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$ + + private static final String PREFS_STACK_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$ + private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$NON-NLS-1$ + + private Display mDisplay; + private Composite mBase; + private Label mNotEnabled; + private Label mNotSelected; + + private Composite mThreadBase; + private Table mThreadTable; + private TableViewer mThreadViewer; + + private Composite mStackTraceBase; + private Button mRefreshStackTraceButton; + private Label mStackTraceTimeLabel; + private StackTracePanel mStackTracePanel; + private Table mStackTraceTable; + + /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */ + private boolean mMustStopRecurringThreadUpdate = false; + /** Flag to tell the recurring thread update to stop running */ + private boolean mRecurringThreadUpdateRunning = false; + + private Object mLock = new Object(); + + private static final String[] THREAD_STATUS = { + "zombie", "running", "timed-wait", "monitor", + "wait", "init", "start", "native", "vmwait" + }; + + /** + * Content Provider to display the threads of a client. + * Expected input is a {@link Client} object. + */ + private static class ThreadContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Client) { + return ((Client)inputElement).getClientData().getThreads(); + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + + /** + * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be + * of type {@link ThreadInfo}. + */ + private static class ThreadLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof ThreadInfo) { + ThreadInfo thread = (ThreadInfo)element; + switch (columnIndex) { + case 0: + return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$ + String.valueOf(thread.getThreadId()); + case 1: + return String.valueOf(thread.getTid()); + case 2: + if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length) + return THREAD_STATUS[thread.getStatus()]; + return "unknown"; + case 3: + return String.valueOf(thread.getUtime()); + case 4: + return String.valueOf(thread.getStime()); + case 5: + return thread.getThreadName(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + final IPreferenceStore store = DdmUiPreferences.getStore(); + + mBase = new Composite(parent, SWT.NONE); + mBase.setLayout(new StackLayout()); + + // UI for thread not enabled + mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP); + mNotEnabled.setText("Thread updates not enabled for selected client\n" + + "(use toolbar button to enable)"); + + // UI for not client selected + mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP); + mNotSelected.setText("no client is selected"); + + // base composite for selected client with enabled thread update. + mThreadBase = new Composite(mBase, SWT.NONE); + mThreadBase.setLayout(new FormLayout()); + + // table above the sash + mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION); + mThreadTable.setHeaderVisible(true); + mThreadTable.setLinesVisible(true); + + TableHelper.createTableColumn( + mThreadTable, + "ID", + SWT.RIGHT, + "888", //$NON-NLS-1$ + PREFS_THREAD_COL_ID, store); + + TableHelper.createTableColumn( + mThreadTable, + "Tid", + SWT.RIGHT, + "88888", //$NON-NLS-1$ + PREFS_THREAD_COL_TID, store); + + TableHelper.createTableColumn( + mThreadTable, + "Status", + SWT.LEFT, + "timed-wait", //$NON-NLS-1$ + PREFS_THREAD_COL_STATUS, store); + + TableHelper.createTableColumn( + mThreadTable, + "utime", + SWT.RIGHT, + "utime", //$NON-NLS-1$ + PREFS_THREAD_COL_UTIME, store); + + TableHelper.createTableColumn( + mThreadTable, + "stime", + SWT.RIGHT, + "utime", //$NON-NLS-1$ + PREFS_THREAD_COL_STIME, store); + + TableHelper.createTableColumn( + mThreadTable, + "Name", + SWT.LEFT, + "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$ + PREFS_THREAD_COL_NAME, store); + + mThreadViewer = new TableViewer(mThreadTable); + mThreadViewer.setContentProvider(new ThreadContentProvider()); + mThreadViewer.setLabelProvider(new ThreadLabelProvider()); + + mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ThreadInfo selectedThread = getThreadSelection(event.getSelection()); + updateThreadStackTrace(selectedThread); + } + }); + mThreadViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + ThreadInfo selectedThread = getThreadSelection(event.getSelection()); + if (selectedThread != null) { + Client client = (Client)mThreadViewer.getInput(); + + if (client != null) { + client.requestThreadStackTrace(selectedThread.getThreadId()); + } + } + } + }); + + // the separating sash + final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL); + Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + sash.setBackground(darkGray); + + // the UI below the sash + mStackTraceBase = new Composite(mThreadBase, SWT.NONE); + mStackTraceBase.setLayout(new GridLayout(2, false)); + + mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH); + mRefreshStackTraceButton.setText("Refresh"); + mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + ThreadInfo selectedThread = getThreadSelection(null); + if (selectedThread != null) { + Client currentClient = getCurrentClient(); + if (currentClient != null) { + currentClient.requestThreadStackTrace(selectedThread.getThreadId()); + } + } + } + }); + + mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE); + mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mStackTracePanel = new StackTracePanel(); + mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, + PREFS_STACK_COL_CLASS, + PREFS_STACK_COL_METHOD, + PREFS_STACK_COL_FILE, + PREFS_STACK_COL_LINE, + PREFS_STACK_COL_NATIVE, + store); + + GridData gd; + mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.horizontalSpan = 2; + + // now setup the sash. + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mThreadTable.setLayoutData(data); + + final FormData sashData = new FormData(); + if (store != null && store.contains(PREFS_THREAD_SASH)) { + sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mStackTraceBase.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = mThreadBase.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + store.setValue(PREFS_THREAD_SASH, e.y); + mThreadBase.layout(); + } + } + }); + + ((StackLayout)mBase.getLayout()).topControl = mNotSelected; + + return mBase; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mThreadTable.setFocus(); + } + + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 || + (changeMask & Client.CHANGE_THREAD_DATA) != 0) { + try { + mThreadTable.getDisplay().asyncExec(new Runnable() { + public void run() { + clientSelected(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) { + try { + mThreadTable.getDisplay().asyncExec(new Runnable() { + public void run() { + updateThreadStackCall(); + } + }); + } catch (SWTException e) { + // widget is disposed, we do nothing + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mThreadTable.isDisposed()) { + return; + } + + Client client = getCurrentClient(); + + mStackTracePanel.setCurrentClient(client); + + if (client != null) { + if (!client.isThreadUpdateEnabled()) { + ((StackLayout)mBase.getLayout()).topControl = mNotEnabled; + mThreadViewer.setInput(null); + + // if we are currently updating the thread, stop doing it. + mMustStopRecurringThreadUpdate = true; + } else { + ((StackLayout)mBase.getLayout()).topControl = mThreadBase; + mThreadViewer.setInput(client); + + synchronized (mLock) { + // if we're not updating we start the process + if (mRecurringThreadUpdateRunning == false) { + startRecurringThreadUpdate(); + } else if (mMustStopRecurringThreadUpdate) { + // else if there's a runnable that's still going to get called, lets + // simply cancel the stop, and keep going + mMustStopRecurringThreadUpdate = false; + } + } + } + } else { + ((StackLayout)mBase.getLayout()).topControl = mNotSelected; + mThreadViewer.setInput(null); + } + + mBase.layout(); + } + + /** + * Updates the stack call of the currently selected thread. + *

+ * This must be called from the UI thread. + */ + private void updateThreadStackCall() { + Client client = getCurrentClient(); + if (client != null) { + // get the current selection in the ThreadTable + ThreadInfo selectedThread = getThreadSelection(null); + + if (selectedThread != null) { + updateThreadStackTrace(selectedThread); + } else { + updateThreadStackTrace(null); + } + } + } + + /** + * updates the stackcall of the specified thread. If null the UI is emptied + * of current data. + * @param thread + */ + private void updateThreadStackTrace(ThreadInfo thread) { + mStackTracePanel.setViewerInput(thread); + + if (thread != null) { + mRefreshStackTraceButton.setEnabled(true); + long stackcallTime = thread.getStackCallTime(); + if (stackcallTime != 0) { + String label = new Date(stackcallTime).toString(); + mStackTraceTimeLabel.setText(label); + } else { + mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ + } + } else { + mRefreshStackTraceButton.setEnabled(true); + mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mThreadTable); + addTableToFocusListener(mStackTraceTable); + } + + /** + * Initiate recurring events. We use a shorter "initialWait" so we do the + * first execution sooner. We don't do it immediately because we want to + * give the clients a chance to get set up. + */ + private void startRecurringThreadUpdate() { + mRecurringThreadUpdateRunning = true; + int initialWait = 1000; + + mDisplay.timerExec(initialWait, new Runnable() { + public void run() { + synchronized (mLock) { + // lets check we still want updates. + if (mMustStopRecurringThreadUpdate == false) { + Client client = getCurrentClient(); + if (client != null) { + client.requestThreadUpdate(); + + mDisplay.timerExec( + DdmUiPreferences.getThreadRefreshInterval() * 1000, this); + } else { + // we don't have a Client, which means the runnable is not + // going to be called through the timer. We reset the running flag. + mRecurringThreadUpdateRunning = false; + } + } else { + // else actually stops (don't call the timerExec) and reset the flags. + mRecurringThreadUpdateRunning = false; + mMustStopRecurringThreadUpdate = false; + } + } + } + }); + } + + /** + * Returns the current thread selection or null if none is found. + * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection + * is returned, otherwise, the ISelection returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or null + */ + private ThreadInfo getThreadSelection(ISelection selection) { + if (selection == null) { + selection = mThreadViewer.getSelection(); + } + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof ThreadInfo) { + return (ThreadInfo)object; + } + } + + return null; + } + +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java new file mode 100644 index 000000000..856b874f9 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ICommonAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2007 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.actions; + +/** + * Common interface for basic action handling. This allows the common ui + * components to access ToolItem or Action the same way. + */ +public interface ICommonAction { + /** + * Sets the enabled state of this action. + * @param enabled true to enable, and + * false to disable + */ + public void setEnabled(boolean enabled); + + /** + * Sets the checked status of this action. + * @param checked the new checked status + */ + public void setChecked(boolean checked); + + /** + * Sets the {@link Runnable} that will be executed when the action is triggered. + */ + public void setRunnable(Runnable runnable); +} + diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java new file mode 100644 index 000000000..bc1598fba --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/actions/ToolItemAction.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2007 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.actions; + +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; + +/** + * Wrapper around {@link ToolItem} to implement {@link ICommonAction} + */ +public class ToolItemAction implements ICommonAction { + public ToolItem item; + + public ToolItemAction(ToolBar parent, int style) { + item = new ToolItem(parent, style); + } + + /** + * Sets the enabled state of this action. + * @param enabled true to enable, and + * false to disable + * @see ICommonAction#setChecked(boolean) + */ + public void setChecked(boolean checked) { + item.setSelection(checked); + } + + /** + * Sets the enabled state of this action. + * @param enabled true to enable, and + * false to disable + * @see ICommonAction#setEnabled(boolean) + */ + public void setEnabled(boolean enabled) { + item.setEnabled(enabled); + } + + /** + * Sets the {@link Runnable} that will be executed when the action is triggered (through + * {@link SelectionListener#widgetSelected(SelectionEvent)} on the wrapped {@link ToolItem}). + * @see ICommonAction#setRunnable(Runnable) + */ + public void setRunnable(final Runnable runnable) { + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + runnable.run(); + } + }); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java new file mode 100644 index 000000000..8e9e11b6b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/UiThread.java @@ -0,0 +1,31 @@ +/* + * 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.annotation; + +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Simple utility annotation used only to mark methods that are executed on the UI thread. + * This annotation's sole purpose is to help reading the source code. It has no additional effect. + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.SOURCE) +public @interface UiThread { +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java new file mode 100644 index 000000000..e767eda7d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/annotation/WorkerThread.java @@ -0,0 +1,31 @@ +/* + * 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.annotation; + +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Simple utility annotation used only to mark methods that are not executed on the UI thread. + * This annotation's sole purpose is to help reading the source code. It has no additional effect. + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.SOURCE) +public @interface WorkerThread { +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java new file mode 100644 index 000000000..4df4376ef --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/DdmConsole.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2007 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.console; + + +/** + * Static Console used to ouput messages. By default outputs the message to System.out and + * System.err, but can receive a IDdmConsole object which will actually do something. + */ +public class DdmConsole { + + private static IDdmConsole mConsole; + + /** + * Prints a message to the android console. + * @param message the message to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printErrorToConsole(String message) { + if (mConsole != null) { + mConsole.printErrorToConsole(message); + } else { + System.err.println(message); + } + } + + /** + * Prints several messages to the android console. + * @param messages the messages to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printErrorToConsole(String[] messages) { + if (mConsole != null) { + mConsole.printErrorToConsole(messages); + } else { + for (String message : messages) { + System.err.println(message); + } + } + } + + /** + * Prints a message to the android console. + * @param message the message to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printToConsole(String message) { + if (mConsole != null) { + mConsole.printToConsole(message); + } else { + System.out.println(message); + } + } + + /** + * Prints several messages to the android console. + * @param messages the messages to print + * @param forceDisplay if true, this force the console to be displayed. + */ + public static void printToConsole(String[] messages) { + if (mConsole != null) { + mConsole.printToConsole(messages); + } else { + for (String message : messages) { + System.out.println(message); + } + } + } + + /** + * Sets a IDdmConsole to override the default behavior of the console + * @param console The new IDdmConsole + * **/ + public static void setConsole(IDdmConsole console) { + mConsole = console; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java new file mode 100644 index 000000000..3679d4131 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/console/IDdmConsole.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2007 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.console; + + +/** + * DDMS console interface. + */ +public interface IDdmConsole { + /** + * Prints a message to the android console. + * @param message the message to print + */ + public void printErrorToConsole(String message); + + /** + * Prints several messages to the android console. + * @param messages the messages to print + */ + public void printErrorToConsole(String[] messages); + + /** + * Prints a message to the android console. + * @param message the message to print + */ + public void printToConsole(String message); + + /** + * Prints several messages to the android console. + * @param messages the messages to print + */ + public void printToConsole(String[] messages); +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java new file mode 100644 index 000000000..75c19fe2f --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceContentProvider.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2007 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.explorer; + +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.FileListingService.IListingReceiver; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; + +/** + * Content provider class for device Explorer. + */ +class DeviceContentProvider implements ITreeContentProvider { + + private TreeViewer mViewer; + private FileListingService mFileListingService; + private FileEntry mRootEntry; + + private IListingReceiver sListingReceiver = new IListingReceiver() { + public void setChildren(final FileEntry entry, FileEntry[] children) { + final Tree t = mViewer.getTree(); + if (t != null && t.isDisposed() == false) { + Display display = t.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (t.isDisposed() == false) { + // refresh the entry. + mViewer.refresh(entry); + + // force it open, since on linux and windows + // when getChildren() returns null, the node is + // not considered expanded. + mViewer.setExpandedState(entry, true); + } + } + }); + } + } + } + + public void refreshEntry(final FileEntry entry) { + final Tree t = mViewer.getTree(); + if (t != null && t.isDisposed() == false) { + Display display = t.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (t.isDisposed() == false) { + // refresh the entry. + mViewer.refresh(entry); + } + } + }); + } + } + } + }; + + /** + * + */ + public DeviceContentProvider() { + } + + public void setListingService(FileListingService fls) { + mFileListingService = fls; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object) + */ + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof FileEntry) { + FileEntry parentEntry = (FileEntry)parentElement; + + Object[] oldEntries = parentEntry.getCachedChildren(); + Object[] newEntries = mFileListingService.getChildren(parentEntry, + true, sListingReceiver); + + if (newEntries != null) { + return newEntries; + } else { + // if null was returned, this means the cache was not valid, + // and a thread was launched for ls. sListingReceiver will be + // notified with the new entries. + return oldEntries; + } + } + return new Object[0]; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object) + */ + public Object getParent(Object element) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + return entry.getParent(); + } + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object) + */ + public boolean hasChildren(Object element) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + return entry.getType() == FileListingService.TYPE_DIRECTORY; + } + return false; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object) + */ + public Object[] getElements(Object inputElement) { + if (inputElement instanceof FileEntry) { + FileEntry entry = (FileEntry)inputElement; + if (entry.isRoot()) { + return getChildren(mRootEntry); + } + } + + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IContentProvider#dispose() + */ + public void dispose() { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object) + */ + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + if (viewer instanceof TreeViewer) { + mViewer = (TreeViewer)viewer; + } + if (newInput instanceof FileEntry) { + mRootEntry = (FileEntry)newInput; + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java new file mode 100644 index 000000000..66843a473 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/DeviceExplorer.java @@ -0,0 +1,791 @@ +/* + * Copyright (C) 2007 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.explorer; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.android.ddmlib.SyncService.SyncResult; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.Panel; +import com.android.ddmuilib.SyncProgressMonitor; +import com.android.ddmuilib.TableHelper; +import com.android.ddmuilib.actions.ICommonAction; +import com.android.ddmuilib.console.DdmConsole; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.ViewerDropAdapter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.FileTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.dnd.TransferData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Device filesystem explorer class. + */ +public class DeviceExplorer extends Panel { + + private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S + private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S + + private static Pattern mKeyFilePattern = Pattern.compile( + "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S + private static Pattern mDataFilePattern = Pattern.compile( + "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S + + public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S + public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S + public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S + public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S + public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S + public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S + + private Composite mParent; + private TreeViewer mTreeViewer; + private Tree mTree; + private DeviceContentProvider mContentProvider; + + private ICommonAction mPushAction; + private ICommonAction mPullAction; + private ICommonAction mDeleteAction; + + private Image mFileImage; + private Image mFolderImage; + private Image mPackageImage; + private Image mOtherImage; + + private IDevice mCurrentDevice; + + private String mDefaultSave; + + public DeviceExplorer() { + + } + + /** + * Sets the images for the listview + * @param fileImage + * @param folderImage + * @param otherImage + */ + public void setImages(Image fileImage, Image folderImage, Image packageImage, + Image otherImage) { + mFileImage = fileImage; + mFolderImage = folderImage; + mPackageImage = packageImage; + mOtherImage = otherImage; + } + + /** + * Sets the actions so that the device explorer can enable/disable them based on the current + * selection + * @param pushAction + * @param pullAction + * @param deleteAction + */ + public void setActions(ICommonAction pushAction, ICommonAction pullAction, + ICommonAction deleteAction) { + mPushAction = pushAction; + mPullAction = pullAction; + mDeleteAction = deleteAction; + } + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + parent.setLayout(new FillLayout()); + + mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL); + mTree.setHeaderVisible(true); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + // create columns + TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, + "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT, + "000000", COLUMN_SIZE, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT, + "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT, + "20:54", COLUMN_TIME, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT, + "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT, + "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$ + + // create the jface wrapper + mTreeViewer = new TreeViewer(mTree); + + // setup data provider + mContentProvider = new DeviceContentProvider(); + mTreeViewer.setContentProvider(mContentProvider); + mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage, + mFolderImage, mPackageImage, mOtherImage)); + + // setup a listener for selection + mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection sel = event.getSelection(); + if (sel.isEmpty()) { + mPullAction.setEnabled(false); + mPushAction.setEnabled(false); + mDeleteAction.setEnabled(false); + return; + } + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + Object element = selection.getFirstElement(); + if (element == null) + return; + if (element instanceof FileEntry) { + mPullAction.setEnabled(true); + mPushAction.setEnabled(selection.size() == 1); + if (selection.size() == 1) { + setDeleteEnabledState((FileEntry)element); + } else { + mDeleteAction.setEnabled(false); + } + } + } + } + }); + + // add support for double click + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + ISelection sel = event.getSelection(); + + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + + if (selection.size() == 1) { + FileEntry entry = (FileEntry)selection.getFirstElement(); + String name = entry.getName(); + + FileEntry parentEntry = entry.getParent(); + + // can't really do anything with no parent + if (parentEntry == null) { + return; + } + + // check this is a file like we want. + Matcher m = mKeyFilePattern.matcher(name); + if (m.matches()) { + // get the name w/o the extension + String baseName = m.group(1); + + // add the data extension + String dataName = baseName + TRACE_DATA_EXT; + + FileEntry dataEntry = parentEntry.findChild(dataName); + + handleTraceDoubleClick(baseName, entry, dataEntry); + + } else { + m = mDataFilePattern.matcher(name); + if (m.matches()) { + // get the name w/o the extension + String baseName = m.group(1); + + // add the key extension + String keyName = baseName + TRACE_KEY_EXT; + + FileEntry keyEntry = parentEntry.findChild(keyName); + + handleTraceDoubleClick(baseName, keyEntry, entry); + } + } + } + } + } + }); + + // setup drop listener + mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, + new Transfer[] { FileTransfer.getInstance() }, + new ViewerDropAdapter(mTreeViewer) { + @Override + public boolean performDrop(Object data) { + // get the item on which we dropped the item(s) + FileEntry target = (FileEntry)getCurrentTarget(); + + // in case we drop at the same level as root + if (target == null) { + return false; + } + + // if the target is not a directory, we get the parent directory + if (target.isDirectory() == false) { + target = target.getParent(); + } + + if (target == null) { + return false; + } + + // get the list of files to drop + String[] files = (String[])data; + + // do the drop + pushFiles(files, target); + + // we need to finish with a refresh + refresh(target); + + return true; + } + + @Override + public boolean validateDrop(Object target, int operation, TransferData transferType) { + if (target == null) { + return false; + } + + // convert to the real item + FileEntry targetEntry = (FileEntry)target; + + // if the target is not a directory, we get the parent directory + if (targetEntry.isDirectory() == false) { + target = targetEntry.getParent(); + } + + if (target == null) { + return false; + } + + return true; + } + }); + + // create and start the refresh thread + new Thread("Device Ls refresher") { + @Override + public void run() { + while (true) { + try { + sleep(FileListingService.REFRESH_RATE); + } catch (InterruptedException e) { + return; + } + + if (mTree != null && mTree.isDisposed() == false) { + Display display = mTree.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + mTreeViewer.refresh(true); + } + } + }); + } else { + return; + } + } else { + return; + } + } + + } + }.start(); + + return mTree; + } + + @Override + protected void postCreation() { + + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTree.setFocus(); + } + + /** + * Processes a double click on a trace file + * @param baseName the base name of the 2 files. + * @param keyEntry The FileEntry for the .key file. + * @param dataEntry The FileEntry for the .data file. + */ + private void handleTraceDoubleClick(String baseName, FileEntry keyEntry, + FileEntry dataEntry) { + // first we need to download the files. + File keyFile; + File dataFile; + String path; + try { + // create a temp file for keyFile + File f = File.createTempFile(baseName, ".trace"); + f.delete(); + f.mkdir(); + + path = f.getAbsolutePath(); + + keyFile = new File(path + File.separator + keyEntry.getName()); + dataFile = new File(path + File.separator + dataEntry.getName()); + } catch (IOException e) { + return; + } + + // download the files + try { + SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor(); + SyncResult result = sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), result.getMessage())); + return; + } + + result = sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", dataEntry.getName(), result.getMessage())); + return; + } + + // now that we have the file, we need to launch traceview + String[] command = new String[2]; + command[0] = DdmUiPreferences.getTraceview(); + command[1] = path + File.separator + baseName; + + try { + final Process p = Runtime.getRuntime().exec(command); + + // create a thread for the output + new Thread("Traceview output") { + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(p.getErrorStream()); + BufferedReader resultReader = new BufferedReader(is); + + // read the lines as they come. if null is returned, it's + // because the process finished + try { + while (true) { + String line = resultReader.readLine(); + if (line != null) { + DdmConsole.printErrorToConsole("Traceview: " + line); + } else { + break; + } + } + // get the return code from the process + p.waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + + } + } + }.start(); + + } catch (IOException e) { + } + } + } catch (IOException e) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); + return; + } + } + + /** + * Pull the current selection on the local drive. This method displays + * a dialog box to let the user select where to store the file(s) and + * folder(s). + */ + public void pullSelection() { + // get the selection + TreeItem[] items = mTree.getSelection(); + + // name of the single file pull, or null if we're pulling a directory + // or more than one object. + String filePullName = null; + FileEntry singleEntry = null; + + // are we pulling a single file? + if (items.length == 1) { + singleEntry = (FileEntry)items[0].getData(); + if (singleEntry.getType() == FileListingService.TYPE_FILE) { + filePullName = singleEntry.getName(); + } + } + + // where do we save by default? + String defaultPath = mDefaultSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + + if (filePullName != null) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); + + fileDialog.setText("Get Device File"); + fileDialog.setFileName(filePullName); + fileDialog.setFilterPath(defaultPath); + + String fileName = fileDialog.open(); + if (fileName != null) { + mDefaultSave = fileDialog.getFilterPath(); + + pullFile(singleEntry, fileName); + } + } else { + DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE); + + directoryDialog.setText("Get Device Files/Folders"); + directoryDialog.setFilterPath(defaultPath); + + String directoryName = directoryDialog.open(); + if (directoryName != null) { + pullSelection(items, directoryName); + } + } + } + + /** + * Push new file(s) and folder(s) into the current selection. Current + * selection must be single item. If the current selection is not a + * directory, the parent directory is used. + * This method displays a dialog to let the user choose file to push to + * the device. + */ + public void pushIntoSelection() { + // get the name of the object we're going to pull + TreeItem[] items = mTree.getSelection(); + + if (items.length == 0) { + return; + } + + FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN); + String fileName; + + dlg.setText("Put File on Device"); + + // There should be only one. + FileEntry entry = (FileEntry)items[0].getData(); + dlg.setFileName(entry.getName()); + + String defaultPath = mDefaultSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + dlg.setFilterPath(defaultPath); + + fileName = dlg.open(); + if (fileName != null) { + mDefaultSave = dlg.getFilterPath(); + + // we need to figure out the remote path based on the current selection type. + String remotePath; + FileEntry toRefresh = entry; + if (entry.isDirectory()) { + remotePath = entry.getFullPath(); + } else { + toRefresh = entry.getParent(); + remotePath = toRefresh.getFullPath(); + } + + pushFile(fileName, remotePath); + mTreeViewer.refresh(toRefresh); + } + } + + public void deleteSelection() { + // get the name of the object we're going to pull + TreeItem[] items = mTree.getSelection(); + + if (items.length != 1) { + return; + } + + FileEntry entry = (FileEntry)items[0].getData(); + final FileEntry parentEntry = entry.getParent(); + + // create the delete command + String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$ + + try { + mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() { + public void addOutput(byte[] data, int offset, int length) { + // pass + // TODO get output to display errors if any. + } + + public void flush() { + mTreeViewer.refresh(parentEntry); + } + + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } + + } + + /** + * Force a full refresh of the explorer. + */ + public void refresh() { + mTreeViewer.refresh(true); + } + + /** + * Sets the new device to explorer + */ + public void switchDevice(final IDevice device) { + if (device != mCurrentDevice) { + mCurrentDevice = device; + // now we change the input. but we need to do that in the + // ui thread. + if (mTree.isDisposed() == false) { + Display d = mTree.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + // new service + if (mCurrentDevice != null) { + FileListingService fls = mCurrentDevice.getFileListingService(); + mContentProvider.setListingService(fls); + mTreeViewer.setInput(fls.getRoot()); + } + } + } + }); + } + } + } + + /** + * Refresh an entry from a non ui thread. + * @param entry the entry to refresh. + */ + private void refresh(final FileEntry entry) { + Display d = mTreeViewer.getTree().getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + mTreeViewer.refresh(entry); + } + }); + } + + /** + * Pulls the selection from a device. + * @param items the tree selection the remote file on the device + * @param localDirector the local directory in which to save the files. + */ + private void pullSelection(TreeItem[] items, final String localDirectory) { + try { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + // make a list of the FileEntry. + ArrayList entries = new ArrayList(); + for (TreeItem item : items) { + Object data = item.getData(); + if (data instanceof FileEntry) { + entries.add((FileEntry)data); + } + } + final FileEntry[] entryArray = entries.toArray( + new FileEntry[entries.size()]); + + // get a progressdialog + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + // create a monitor wrapper around the jface monitor + SyncResult result = sync.pull(entryArray, localDirectory, + new SyncProgressMonitor(monitor, + "Pulling file(s) from the device")); + + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull selection: %1$s", result.getMessage())); + } + sync.close(); + } + }); + } + } catch (Exception e) { + DdmConsole.printErrorToConsole( "Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + + /** + * Pulls a file from a device. + * @param remote the remote file on the device + * @param local the destination filepath + */ + private void pullFile(final FileEntry remote, final String local) { + try { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + SyncResult result = sync.pullFile(remote, local, new SyncProgressMonitor( + monitor, String.format("Pulling %1$s from the device", + remote.getName()))); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", remote, result.getMessage())); + } + + sync.close(); + } + }); + } + } catch (Exception e) { + DdmConsole.printErrorToConsole( "Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + + /** + * Pushes several files and directory into a remote directory. + * @param localFiles + * @param remoteDirectory + */ + private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) { + try { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + SyncResult result = sync.push(localFiles, remoteDirectory, + new SyncProgressMonitor(monitor, + "Pushing file(s) to the device")); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push the items: %1$s", result.getMessage())); + } + + sync.close(); + } + }); + } + } catch (Exception e) { + DdmConsole.printErrorToConsole("Failed to push the items"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + + /** + * Pushes a file on a device. + * @param local the local filepath of the file to push + * @param remoteDirectory the remote destination directory on the device + */ + private void pushFile(final String local, final String remoteDirectory) { + try { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + new ProgressMonitorDialog(mParent.getShell()).run(true, true, + new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) + throws InvocationTargetException, + InterruptedException { + // get the file name + String[] segs = local.split(Pattern.quote(File.separator)); + String name = segs[segs.length-1]; + String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR + + name; + + SyncResult result = sync.pushFile(local, remoteFile, + new SyncProgressMonitor(monitor, + String.format("Pushing %1$s to the device.", name))); + if (result.getCode() != SyncService.RESULT_OK) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push %1$s on %2$s: %3$s", + name, mCurrentDevice.getSerialNumber(), result.getMessage())); + } + + sync.close(); + } + }); + } + } catch (Exception e) { + DdmConsole.printErrorToConsole("Failed to push the item(s)."); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + + /** + * Sets the enabled state based on a FileEntry properties + * @param element The selected FileEntry + */ + protected void setDeleteEnabledState(FileEntry element) { + mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java new file mode 100644 index 000000000..1dca96284 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/explorer/FileLabelProvider.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2007 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.explorer; + +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.FileListingService.FileEntry; + +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; + +/** + * Label provider for the FileEntry. + */ +class FileLabelProvider implements ILabelProvider, ITableLabelProvider { + + private Image mFileImage; + private Image mFolderImage; + private Image mPackageImage; + private Image mOtherImage; + + /** + * Creates Label provider with custom images. + * @param fileImage the Image to represent a file + * @param folderImage the Image to represent a folder + * @param packageImage the Image to represent a .apk file. If null, + * fileImage is used instead. + * @param otherImage the Image to represent all other entry type. + */ + public FileLabelProvider(Image fileImage, Image folderImage, + Image packageImage, Image otherImage) { + mFileImage = fileImage; + mFolderImage = folderImage; + mOtherImage = otherImage; + if (packageImage != null) { + mPackageImage = packageImage; + } else { + mPackageImage = fileImage; + } + } + + /** + * Creates a label provider with default images. + * + */ + public FileLabelProvider() { + + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object) + */ + public Image getImage(Object element) { + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object) + */ + public String getText(Object element) { + return null; + } + + public Image getColumnImage(Object element, int columnIndex) { + if (columnIndex == 0) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + switch (entry.getType()) { + case FileListingService.TYPE_FILE: + case FileListingService.TYPE_LINK: + // get the name and extension + if (entry.isApplicationPackage()) { + return mPackageImage; + } + return mFileImage; + case FileListingService.TYPE_DIRECTORY: + case FileListingService.TYPE_DIRECTORY_LINK: + return mFolderImage; + } + } + + // default case return a different image. + return mOtherImage; + } + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof FileEntry) { + FileEntry entry = (FileEntry)element; + + switch (columnIndex) { + case 0: + return entry.getName(); + case 1: + return entry.getSize(); + case 2: + return entry.getDate(); + case 3: + return entry.getTime(); + case 4: + return entry.getPermissions(); + case 5: + return entry.getInfo(); + } + } + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener) + */ + public void addListener(ILabelProviderListener listener) { + // we don't need listeners. + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose() + */ + public void dispose() { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String) + */ + public boolean isLabelProperty(Object element, String property) { + return false; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener) + */ + public void removeListener(ILabelProviderListener listener) { + // we don't need listeners + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/handler/BaseFileHandler.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/handler/BaseFileHandler.java new file mode 100644 index 000000000..6c086db29 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/handler/BaseFileHandler.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2009 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.handler; + +import com.android.ddmlib.SyncService; +import com.android.ddmlib.ClientData.IHprofDumpHandler; +import com.android.ddmlib.ClientData.IMethodProfilingHandler; +import com.android.ddmlib.SyncService.SyncResult; +import com.android.ddmuilib.SyncProgressMonitor; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +/** + * Base handler class for handler dealing with files located on a device. + * + * @see IHprofDumpHandler + * @see IMethodProfilingHandler + */ +public abstract class BaseFileHandler { + + protected final Shell mParentShell; + + public BaseFileHandler(Shell parentShell) { + mParentShell = parentShell; + } + + protected abstract String getDialogTitle(); + + /** + * Prompts the user for a save location and pulls the remote files into this location. + *

This must be called from the UI Thread. + * @param sync the {@link SyncService} to use to pull the file from the device + * @param localFileName The default local name + * @param remoteFilePath The name of the file to pull off of the device + * @param title The title of the File Save dialog. + * @return The result of the pull as a {@link SyncResult} object, or null if the sync + * didn't happen (canceled by the user). + * @throws InvocationTargetException + * @throws InterruptedException + */ + protected SyncResult promptAndPull(SyncService sync, + String localFileName, String remoteFilePath, String title) + throws InvocationTargetException, InterruptedException { + FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE); + + fileDialog.setText(title); + fileDialog.setFileName(localFileName); + + String localFilePath = fileDialog.open(); + if (localFilePath != null) { + return pull(sync, localFilePath, remoteFilePath); + } + + return null; + } + + /** + * Prompts the user for a save location and copies a temp file into it. + *

This must be called from the UI Thread. + * @param localFileName The default local name + * @param tempFilePath The name of the temp file to copy. + * @param title The title of the File Save dialog. + * @return true if success, false on error or cancel. + */ + protected boolean promptAndSave(String localFileName, byte[] data, String title) { + FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE); + + fileDialog.setText(title); + fileDialog.setFileName(localFileName); + + String localFilePath = fileDialog.open(); + if (localFilePath != null) { + try { + saveFile(data, new File(localFilePath)); + return true; + } catch (IOException e) { + String errorMsg = e.getMessage(); + displayErrorInUiThread( + "Failed to save file '%1$s'%2$s", + localFilePath, + errorMsg != null ? ":\n" + errorMsg : "."); + } + } + + return false; + } + + /** + * Pulls a file off of a device. This displays a {@link ProgressMonitorDialog} and therefore + * must be run from the UI Thread. + * @param sync the {@link SyncService} to use to pull the file. + * @param localFilePath the path of the local file to create + * @param remoteFilePath the path of the remote file to pull + * @return the result of the sync as an instance of {@link SyncResult} + * @throws InvocationTargetException + * @throws InterruptedException + */ + protected SyncResult pull(final SyncService sync, final String localFilePath, + final String remoteFilePath) + throws InvocationTargetException, InterruptedException { + final SyncResult[] res = new SyncResult[1]; + new ProgressMonitorDialog(mParentShell).run(true, true, new IRunnableWithProgress() { + public void run(IProgressMonitor monitor) { + try { + res[0] = sync.pullFile(remoteFilePath, localFilePath, + new SyncProgressMonitor(monitor, String.format( + "Pulling %1$s from the device", remoteFilePath))); + } finally { + sync.close(); + } + } + }); + + return res[0]; + } + + /** + * Display an error message. + *

This will call about to {@link Display} to run this in an async {@link Runnable} in the + * UI Thread. This is safe to be called from a non-UI Thread. + * @param format the string to display + * @param args the string arguments + */ + protected void displayErrorInUiThread(final String format, final Object... args) { + mParentShell.getDisplay().asyncExec(new Runnable() { + public void run() { + MessageDialog.openError(mParentShell, getDialogTitle(), + String.format(format, args)); + } + }); + } + + /** + * Display an error message. + * This must be called from the UI Thread. + * @param format the string to display + * @param args the string arguments + */ + protected void displayErrorFromUiThread(final String format, final Object... args) { + MessageDialog.openError(mParentShell, getDialogTitle(), + String.format(format, args)); + } + + /** + * Saves a given data into a temp file and returns its corresponding {@link File} object. + * @param data the data to save + * @return the File into which the data was written or null if it failed. + * @throws IOException + */ + protected File saveTempFile(byte[] data) throws IOException { + File f = File.createTempFile("ddms", null); + saveFile(data, f); + return f; + } + + /** + * Saves some data into a given File. + * @param data the data to save + * @param output the file into the data is saved. + * @throws IOException + */ + protected void saveFile(byte[] data, File output) throws IOException { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(output); + fos.write(data); + } finally { + if (fos != null) { + fos.close(); + } + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/handler/MethodProfilingHandler.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/handler/MethodProfilingHandler.java new file mode 100644 index 000000000..b1d7f2a59 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/handler/MethodProfilingHandler.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2009 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.handler; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.ClientData.IMethodProfilingHandler; +import com.android.ddmlib.SyncService.SyncResult; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.console.DdmConsole; + +import org.eclipse.swt.widgets.Shell; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; + +/** + * Handler for Method tracing. + * This will pull the trace file into a temp file and launch traceview. + */ +public class MethodProfilingHandler extends BaseFileHandler + implements IMethodProfilingHandler { + + public MethodProfilingHandler(Shell parentShell) { + super(parentShell); + } + + @Override + protected String getDialogTitle() { + return "Method Profiling Error"; + } + + public void onStartFailure(final Client client, final String message) { + displayErrorInUiThread( + "Unable to create Method Profiling file for application '%1$s'\n\n%2$s" + + "Check logcat for more information.", + client.getClientData().getClientDescription(), + message != null ? message + "\n\n" : ""); + } + + public void onEndFailure(final Client client, final String message) { + displayErrorInUiThread( + "Unable to finish Method Profiling for application '%1$s'\n\n%2$s" + + "Check logcat for more information.", + client.getClientData().getClientDescription(), + message != null ? message + "\n\n" : ""); + } + + public void onSuccess(final String remoteFilePath, final Client client) { + mParentShell.getDisplay().asyncExec(new Runnable() { + public void run() { + if (remoteFilePath == null) { + displayErrorFromUiThread( + "Unable to download trace file: unknown file name.\n" + + "This can happen if you disconnected the device while recording the trace."); + return; + } + + final IDevice device = client.getDevice(); + try { + // get the sync service to pull the HPROF file + final SyncService sync = client.getDevice().getSyncService(); + if (sync != null) { + pullAndOpen(sync, remoteFilePath); + } else { + displayErrorFromUiThread("Unable to download trace file from device '%1$s'.", + device.getSerialNumber()); + } + } catch (Exception e) { + displayErrorFromUiThread("Unable to download trace file from device '%1$s'.", + device.getSerialNumber()); + } + } + + }); + } + + public void onSuccess(byte[] data, final Client client) { + try { + File tempFile = saveTempFile(data); + openInTraceview(tempFile.getAbsolutePath()); + } catch (IOException e) { + String errorMsg = e.getMessage(); + displayErrorInUiThread( + "Failed to save trace data into temp file%1$s", + errorMsg != null ? ":\n" + errorMsg : "."); + } + } + + /** + * pulls and open a file. This is run from the UI thread. + */ + private void pullAndOpen(SyncService sync, String remoteFilePath) + throws InvocationTargetException, InterruptedException, IOException { + // get a temp file + File temp = File.createTempFile("android", ".trace"); //$NON-NLS-1$ //$NON-NLS-2$ + String tempPath = temp.getAbsolutePath(); + + // pull the file + SyncResult result = pull(sync, tempPath, remoteFilePath); + if (result != null) { + if (result.getCode() == SyncService.RESULT_OK) { + // open the temp file in traceview + openInTraceview(tempPath); + } else { + displayErrorFromUiThread("Unable to download trace file:\n\n%1$s", + result.getMessage()); + } + } else { + // this really shouldn't happen. + displayErrorFromUiThread("Unable to download trace file."); + } + } + + private void openInTraceview(String tempPath) { + // now that we have the file, we need to launch traceview + String[] command = new String[2]; + command[0] = DdmUiPreferences.getTraceview(); + command[1] = tempPath; + + try { + final Process p = Runtime.getRuntime().exec(command); + + // create a thread for the output + new Thread("Traceview output") { + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(p.getErrorStream()); + BufferedReader resultReader = new BufferedReader(is); + + // read the lines as they come. if null is returned, it's + // because the process finished + try { + while (true) { + String line = resultReader.readLine(); + if (line != null) { + DdmConsole.printErrorToConsole("Traceview: " + line); + } else { + break; + } + } + // get the return code from the process + p.waitFor(); + } catch (Exception e) { + Log.e("traceview", e); + } + } + }.start(); + } catch (IOException e) { + Log.e("traceview", e); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java new file mode 100644 index 000000000..578a7ac4a --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/CoordinateControls.java @@ -0,0 +1,243 @@ +/* + * 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.location; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; + +/** + * Encapsulation of controls handling a location coordinate in decimal and sexagesimal. + *

This handle the conversion between both modes automatically by using a {@link ModifyListener} + * on all the {@link Text} widgets. + *

To get/set the coordinate, use {@link #setValue(double)} and {@link #getValue()} (preceded by + * a call to {@link #isValueValid()}) + */ +public final class CoordinateControls { + private double mValue; + private boolean mValueValidity = false; + private Text mDecimalText; + private Text mSexagesimalDegreeText; + private Text mSexagesimalMinuteText; + private Text mSexagesimalSecondText; + + /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)} + * is called. This is an int instead of a boolean to act as a counter. */ + private int mManualTextChange = 0; + + /** + * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode. + */ + private ModifyListener mSexagesimalListener = new ModifyListener() { + public void modifyText(ModifyEvent event) { + if (mManualTextChange > 0) { + return; + } + try { + mValue = getValueFromSexagesimalControls(); + setValueIntoDecimalControl(mValue); + mValueValidity = true; + } catch (NumberFormatException e) { + // wrong format empty the decimal controls. + mValueValidity = false; + resetDecimalControls(); + } + } + }; + + /** + * Creates the {@link Text} control for the decimal display of the coordinate. + *

The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createDecimalText(Composite parent) { + mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() { + public void modifyText(ModifyEvent event) { + if (mManualTextChange > 0) { + return; + } + try { + mValue = Double.parseDouble(mDecimalText.getText()); + setValueIntoSexagesimalControl(mValue); + mValueValidity = true; + } catch (NumberFormatException e) { + // wrong format empty the sexagesimal controls. + mValueValidity = false; + resetSexagesimalControls(); + } + } + }); + } + + /** + * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal + * mode. + *

The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalDegreeText(Composite parent) { + mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal + * mode. + *

The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalMinuteText(Composite parent) { + mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal + * mode. + *

The control is expected to be placed in a Composite using a {@link GridLayout}. + * @param parent The {@link Composite} parent of the control. + */ + public void createSexagesimalSecondText(Composite parent) { + mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$ + } + + /** + * Sets the coordinate into the {@link Text} controls. + * @param value the coordinate value to set. + */ + public void setValue(double value) { + mValue = value; + mValueValidity = true; + setValueIntoDecimalControl(value); + setValueIntoSexagesimalControl(value); + } + + /** + * Returns whether the value in the control(s) is valid. + */ + public boolean isValueValid() { + return mValueValidity; + } + + /** + * Returns the current value set in the control(s). + *

This value can be erroneous, and a check with {@link #isValueValid()} should be performed + * before any call to this method. + */ + public double getValue() { + return mValue; + } + + /** + * Enables or disables all the {@link Text} controls. + * @param enabled the enabled state. + */ + public void setEnabled(boolean enabled) { + mDecimalText.setEnabled(enabled); + mSexagesimalDegreeText.setEnabled(enabled); + mSexagesimalMinuteText.setEnabled(enabled); + mSexagesimalSecondText.setEnabled(enabled); + } + + private void resetDecimalControls() { + mManualTextChange++; + mDecimalText.setText(""); //$NON-NLS-1$ + mManualTextChange--; + } + + private void resetSexagesimalControls() { + mManualTextChange++; + mSexagesimalDegreeText.setText(""); //$NON-NLS-1$ + mSexagesimalMinuteText.setText(""); //$NON-NLS-1$ + mSexagesimalSecondText.setText(""); //$NON-NLS-1$ + mManualTextChange--; + } + + /** + * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener} + * @param parent the parent {@link Composite}. + * @param defaultString the default string to be used to compute the {@link Text} control + * size hint. + * @param listener the {@link ModifyListener} to be called when the {@link Text} control is + * modified. + */ + private Text createTextControl(Composite parent, String defaultString, + ModifyListener listener) { + // create the control + Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + + // add the standard listener to it. + text.addModifyListener(listener); + + // compute its size/ + mManualTextChange++; + text.setText(defaultString); + text.pack(); + Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT); + text.setText(""); //$NON-NLS-1$ + mManualTextChange--; + + GridData gridData = new GridData(); + gridData.widthHint = size.x; + text.setLayoutData(gridData); + + return text; + } + + private double getValueFromSexagesimalControls() throws NumberFormatException { + double degrees = Double.parseDouble(mSexagesimalDegreeText.getText()); + double minutes = Double.parseDouble(mSexagesimalMinuteText.getText()); + double seconds = Double.parseDouble(mSexagesimalSecondText.getText()); + + boolean isPositive = (degrees >= 0.); + degrees = Math.abs(degrees); + + double value = degrees + minutes / 60. + seconds / 3600.; + return isPositive ? value : - value; + } + + private void setValueIntoDecimalControl(double value) { + mManualTextChange++; + mDecimalText.setText(String.format("%.6f", value)); + mManualTextChange--; + } + + private void setValueIntoSexagesimalControl(double value) { + // get the sign and make the number positive no matter what. + boolean isPositive = (value >= 0.); + value = Math.abs(value); + + // get the degree + double degrees = Math.floor(value); + + // get the minutes + double minutes = Math.floor((value - degrees) * 60.); + + // get the seconds. + double seconds = (value - degrees) * 3600. - minutes * 60.; + + mManualTextChange++; + mSexagesimalDegreeText.setText( + Integer.toString(isPositive ? (int)degrees : (int)- degrees)); + mSexagesimalMinuteText.setText(Integer.toString((int)minutes)); + mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$ + mManualTextChange--; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java new file mode 100644 index 000000000..a30337a71 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/GpxParser.java @@ -0,0 +1,373 @@ +/* + * 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.location; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * A very basic GPX parser to meet the need of the emulator control panel. + *

+ * It parses basic waypoint information, and tracks (merging segments). + */ +public class GpxParser { + + private final static String NS_GPX = "http://www.topografix.com/GPX/1/1"; //$NON-NLS-1$ + + private final static String NODE_WAYPOINT = "wpt"; //$NON-NLS-1$ + private final static String NODE_TRACK = "trk"; //$NON-NLS-1$ + private final static String NODE_TRACK_SEGMENT = "trkseg"; //$NON-NLS-1$ + private final static String NODE_TRACK_POINT = "trkpt"; //$NON-NLS-1$ + private final static String NODE_NAME = "name"; //$NON-NLS-1$ + private final static String NODE_TIME = "time"; //$NON-NLS-1$ + private final static String NODE_ELEVATION = "ele"; //$NON-NLS-1$ + private final static String NODE_DESCRIPTION = "desc"; //$NON-NLS-1$ + private final static String ATTR_LONGITUDE = "lon"; //$NON-NLS-1$ + private final static String ATTR_LATITUDE = "lat"; //$NON-NLS-1$ + + private static SAXParserFactory sParserFactory; + + static { + sParserFactory = SAXParserFactory.newInstance(); + sParserFactory.setNamespaceAware(true); + } + + private String mFileName; + + private GpxHandler mHandler; + + /** Pattern to parse time with optional sub-second precision, and optional + * Z indicating the time is in UTC. */ + private final static Pattern ISO8601_TIME = + Pattern.compile("(\\d{4})-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:(\\.\\d+))?(Z)?"); //$NON-NLS-1$ + + /** + * Handler for the SAX parser. + */ + private static class GpxHandler extends DefaultHandler { + // --------- parsed data --------- + List mWayPoints; + List mTrackList; + + // --------- state for parsing --------- + Track mCurrentTrack; + TrackPoint mCurrentTrackPoint; + WayPoint mCurrentWayPoint; + final StringBuilder mStringAccumulator = new StringBuilder(); + + boolean mSuccess = true; + + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + // we only care about the standard GPX nodes. + try { + if (NS_GPX.equals(uri)) { + if (NODE_WAYPOINT.equals(localName)) { + if (mWayPoints == null) { + mWayPoints = new ArrayList(); + } + + mWayPoints.add(mCurrentWayPoint = new WayPoint()); + handleLocation(mCurrentWayPoint, attributes); + } else if (NODE_TRACK.equals(localName)) { + if (mTrackList == null) { + mTrackList = new ArrayList(); + } + + mTrackList.add(mCurrentTrack = new Track()); + } else if (NODE_TRACK_SEGMENT.equals(localName)) { + // for now we do nothing here. This will merge all the segments into + // a single TrackPoint list in the Track. + } else if (NODE_TRACK_POINT.equals(localName)) { + if (mCurrentTrack != null) { + mCurrentTrack.addPoint(mCurrentTrackPoint = new TrackPoint()); + handleLocation(mCurrentTrackPoint, attributes); + } + } + } + } finally { + // no matter the node, we empty the StringBuilder accumulator when we start + // a new node. + mStringAccumulator.setLength(0); + } + } + + /** + * Processes new characters for the node content. The characters are simply stored, + * and will be processed when {@link #endElement(String, String, String)} is called. + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + mStringAccumulator.append(ch, start, length); + } + + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (NS_GPX.equals(uri)) { + if (NODE_WAYPOINT.equals(localName)) { + mCurrentWayPoint = null; + } else if (NODE_TRACK.equals(localName)) { + mCurrentTrack = null; + } else if (NODE_TRACK_POINT.equals(localName)) { + mCurrentTrackPoint = null; + } else if (NODE_NAME.equals(localName)) { + if (mCurrentTrack != null) { + mCurrentTrack.setName(mStringAccumulator.toString()); + } else if (mCurrentWayPoint != null) { + mCurrentWayPoint.setName(mStringAccumulator.toString()); + } + } else if (NODE_TIME.equals(localName)) { + if (mCurrentTrackPoint != null) { + mCurrentTrackPoint.setTime(computeTime(mStringAccumulator.toString())); + } + } else if (NODE_ELEVATION.equals(localName)) { + if (mCurrentTrackPoint != null) { + mCurrentTrackPoint.setElevation( + Double.parseDouble(mStringAccumulator.toString())); + } else if (mCurrentWayPoint != null) { + mCurrentWayPoint.setElevation( + Double.parseDouble(mStringAccumulator.toString())); + } + } else if (NODE_DESCRIPTION.equals(localName)) { + if (mCurrentWayPoint != null) { + mCurrentWayPoint.setDescription(mStringAccumulator.toString()); + } + } + } + } + + @Override + public void error(SAXParseException e) throws SAXException { + mSuccess = false; + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + mSuccess = false; + } + + /** + * Converts the string description of the time into milliseconds since epoch. + * @param timeString the string data. + * @return date in milliseconds. + */ + private long computeTime(String timeString) { + // Time looks like: 2008-04-05T19:24:50Z + Matcher m = ISO8601_TIME.matcher(timeString); + if (m.matches()) { + // get the various elements and reconstruct time as a long. + try { + int year = Integer.parseInt(m.group(1)); + int month = Integer.parseInt(m.group(2)); + int date = Integer.parseInt(m.group(3)); + int hourOfDay = Integer.parseInt(m.group(4)); + int minute = Integer.parseInt(m.group(5)); + int second = Integer.parseInt(m.group(6)); + + // handle the optional parameters. + int milliseconds = 0; + + String subSecondGroup = m.group(7); + if (subSecondGroup != null) { + milliseconds = (int)(1000 * Double.parseDouble(subSecondGroup)); + } + + boolean utcTime = m.group(8) != null; + + // now we convert into milliseconds since epoch. + Calendar c; + if (utcTime) { + c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$ + } else { + c = Calendar.getInstance(); + } + + c.set(year, month, date, hourOfDay, minute, second); + + return c.getTimeInMillis() + milliseconds; + } catch (NumberFormatException e) { + // format is invalid, we'll return -1 below. + } + + } + + // invalid time! + return -1; + } + + /** + * Handles the location attributes and store them into a {@link LocationPoint}. + * @param locationNode the {@link LocationPoint} to receive the location data. + * @param attributes the attributes from the XML node. + */ + private void handleLocation(LocationPoint locationNode, Attributes attributes) { + try { + double longitude = Double.parseDouble(attributes.getValue(ATTR_LONGITUDE)); + double latitude = Double.parseDouble(attributes.getValue(ATTR_LATITUDE)); + + locationNode.setLocation(longitude, latitude); + } catch (NumberFormatException e) { + // wrong data, do nothing. + } + } + + WayPoint[] getWayPoints() { + if (mWayPoints != null) { + return mWayPoints.toArray(new WayPoint[mWayPoints.size()]); + } + + return null; + } + + Track[] getTracks() { + if (mTrackList != null) { + return mTrackList.toArray(new Track[mTrackList.size()]); + } + + return null; + } + + boolean getSuccess() { + return mSuccess; + } + } + + /** + * A GPS track. + *

A track is composed of a list of {@link TrackPoint} and optional name and comment. + */ + public final static class Track { + private String mName; + private String mComment; + private List mPoints = new ArrayList(); + + void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + void setComment(String comment) { + mComment = comment; + } + + public String getComment() { + return mComment; + } + + void addPoint(TrackPoint trackPoint) { + mPoints.add(trackPoint); + } + + public TrackPoint[] getPoints() { + return mPoints.toArray(new TrackPoint[mPoints.size()]); + } + + public long getFirstPointTime() { + if (mPoints.size() > 0) { + return mPoints.get(0).getTime(); + } + + return -1; + } + + public long getLastPointTime() { + if (mPoints.size() > 0) { + return mPoints.get(mPoints.size()-1).getTime(); + } + + return -1; + } + + public int getPointCount() { + return mPoints.size(); + } + } + + /** + * Creates a new GPX parser for a file specified by its full path. + * @param fileName The full path of the GPX file to parse. + */ + public GpxParser(String fileName) { + mFileName = fileName; + } + + /** + * Parses the GPX file. + * @return true if success. + */ + public boolean parse() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + mHandler = new GpxHandler(); + + parser.parse(new InputSource(new FileReader(mFileName)), mHandler); + + return mHandler.getSuccess(); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return false; + } + + /** + * Returns the parsed {@link WayPoint} objects, or null if none were found (or + * if the parsing failed. + */ + public WayPoint[] getWayPoints() { + if (mHandler != null) { + return mHandler.getWayPoints(); + } + + return null; + } + + /** + * Returns the parsed {@link Track} objects, or null if none were found (or + * if the parsing failed. + */ + public Track[] getTracks() { + if (mHandler != null) { + return mHandler.getTracks(); + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java new file mode 100644 index 000000000..af485ac1c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/KmlParser.java @@ -0,0 +1,210 @@ +/* + * 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.location; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * A very basic KML parser to meet the need of the emulator control panel. + *

+ * It parses basic Placemark information. + */ +public class KmlParser { + + private final static String NS_KML_2 = "http://earth.google.com/kml/2."; //$NON-NLS-1$ + + private final static String NODE_PLACEMARK = "Placemark"; //$NON-NLS-1$ + private final static String NODE_NAME = "name"; //$NON-NLS-1$ + private final static String NODE_COORDINATES = "coordinates"; //$NON-NLS-1$ + + private final static Pattern sLocationPattern = Pattern.compile("([^,]+),([^,]+)(?:,([^,]+))?"); + + private static SAXParserFactory sParserFactory; + + static { + sParserFactory = SAXParserFactory.newInstance(); + sParserFactory.setNamespaceAware(true); + } + + private String mFileName; + + private KmlHandler mHandler; + + /** + * Handler for the SAX parser. + */ + private static class KmlHandler extends DefaultHandler { + // --------- parsed data --------- + List mWayPoints; + + // --------- state for parsing --------- + WayPoint mCurrentWayPoint; + final StringBuilder mStringAccumulator = new StringBuilder(); + + boolean mSuccess = true; + + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + // we only care about the standard GPX nodes. + try { + if (uri.startsWith(NS_KML_2)) { + if (NODE_PLACEMARK.equals(localName)) { + if (mWayPoints == null) { + mWayPoints = new ArrayList(); + } + + mWayPoints.add(mCurrentWayPoint = new WayPoint()); + } + } + } finally { + // no matter the node, we empty the StringBuilder accumulator when we start + // a new node. + mStringAccumulator.setLength(0); + } + } + + /** + * Processes new characters for the node content. The characters are simply stored, + * and will be processed when {@link #endElement(String, String, String)} is called. + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + mStringAccumulator.append(ch, start, length); + } + + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (uri.startsWith(NS_KML_2)) { + if (NODE_PLACEMARK.equals(localName)) { + mCurrentWayPoint = null; + } else if (NODE_NAME.equals(localName)) { + if (mCurrentWayPoint != null) { + mCurrentWayPoint.setName(mStringAccumulator.toString()); + } + } else if (NODE_COORDINATES.equals(localName)) { + if (mCurrentWayPoint != null) { + parseLocation(mCurrentWayPoint, mStringAccumulator.toString()); + } + } + } + } + + @Override + public void error(SAXParseException e) throws SAXException { + mSuccess = false; + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + mSuccess = false; + } + + /** + * Parses the location string and store the information into a {@link LocationPoint}. + * @param locationNode the {@link LocationPoint} to receive the location data. + * @param location The string containing the location info. + */ + private void parseLocation(LocationPoint locationNode, String location) { + Matcher m = sLocationPattern.matcher(location); + if (m.matches()) { + try { + double longitude = Double.parseDouble(m.group(1)); + double latitude = Double.parseDouble(m.group(2)); + + locationNode.setLocation(longitude, latitude); + + if (m.groupCount() == 3) { + // looks like we have elevation data. + locationNode.setElevation(Double.parseDouble(m.group(3))); + } + } catch (NumberFormatException e) { + // wrong data, do nothing. + } + } + } + + WayPoint[] getWayPoints() { + if (mWayPoints != null) { + return mWayPoints.toArray(new WayPoint[mWayPoints.size()]); + } + + return null; + } + + boolean getSuccess() { + return mSuccess; + } + } + + /** + * Creates a new GPX parser for a file specified by its full path. + * @param fileName The full path of the GPX file to parse. + */ + public KmlParser(String fileName) { + mFileName = fileName; + } + + /** + * Parses the GPX file. + * @return true if success. + */ + public boolean parse() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + mHandler = new KmlHandler(); + + parser.parse(new InputSource(new FileReader(mFileName)), mHandler); + + return mHandler.getSuccess(); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return false; + } + + /** + * Returns the parsed {@link WayPoint} objects, or null if none were found (or + * if the parsing failed. + */ + public WayPoint[] getWayPoints() { + if (mHandler != null) { + return mHandler.getWayPoints(); + } + + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java new file mode 100644 index 000000000..dbb8f4172 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/LocationPoint.java @@ -0,0 +1,53 @@ +/* + * 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.location; + +/** + * Base class for Location aware points. + */ +class LocationPoint { + private double mLongitude; + private double mLatitude; + private boolean mHasElevation = false; + private double mElevation; + + final void setLocation(double longitude, double latitude) { + mLongitude = longitude; + mLatitude = latitude; + } + + public final double getLongitude() { + return mLongitude; + } + + public final double getLatitude() { + return mLatitude; + } + + final void setElevation(double elevation) { + mElevation = elevation; + mHasElevation = true; + } + + public final boolean hasElevation() { + return mHasElevation; + } + + public final double getElevation() { + return mElevation; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java new file mode 100644 index 000000000..7fb37ce95 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackContentProvider.java @@ -0,0 +1,45 @@ +/* + * 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.location; + +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Content provider to display {@link Track} objects in a Table. + *

The expected type for the input is {@link Track}[]. + */ +public class TrackContentProvider implements IStructuredContentProvider { + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Track[]) { + return (Track[])inputElement; + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java new file mode 100644 index 000000000..81d1f7dc1 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackLabelProvider.java @@ -0,0 +1,81 @@ +/* + * 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.location; + +import com.android.ddmuilib.location.GpxParser.Track; + +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Table; + +import java.util.Date; + +/** + * Label Provider for {@link Table} objects displaying {@link Track} objects. + */ +public class TrackLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof Track) { + Track track = (Track)element; + switch (columnIndex) { + case 0: + return track.getName(); + case 1: + return Integer.toString(track.getPointCount()); + case 2: + long time = track.getFirstPointTime(); + if (time != -1) { + return new Date(time).toString(); + } + break; + case 3: + time = track.getLastPointTime(); + if (time != -1) { + return new Date(time).toString(); + } + break; + case 4: + return track.getComment(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java new file mode 100644 index 000000000..527f4bf9b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/TrackPoint.java @@ -0,0 +1,34 @@ +/* + * 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.location; + + +/** + * A Track Point. + *

A track point is a point in time and space. + */ +public class TrackPoint extends LocationPoint { + private long mTime; + + void setTime(long time) { + mTime = time; + } + + public long getTime() { + return mTime; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java new file mode 100644 index 000000000..32880bde4 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPoint.java @@ -0,0 +1,42 @@ +/* + * 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.location; + +/** + * A GPS/KML way point. + *

A waypoint is a user specified location, with a name and an optional description. + */ +public final class WayPoint extends LocationPoint { + private String mName; + private String mDescription; + + void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + void setDescription(String description) { + mDescription = description; + } + + public String getDescription() { + return mDescription; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java new file mode 100644 index 000000000..fced77706 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointContentProvider.java @@ -0,0 +1,43 @@ +/* + * 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.location; + +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Content provider to display {@link WayPoint} objects in a Table. + *

The expected type for the input is {@link WayPoint}[]. + */ +public class WayPointContentProvider implements IStructuredContentProvider { + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof WayPoint[]) { + return (WayPoint[])inputElement; + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java new file mode 100644 index 000000000..f5e6f1b6d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/location/WayPointLabelProvider.java @@ -0,0 +1,73 @@ +/* + * 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.location; + +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Table; + +/** + * Label Provider for {@link Table} objects displaying {@link WayPoint} objects. + */ +public class WayPointLabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof WayPoint) { + WayPoint wayPoint = (WayPoint)element; + switch (columnIndex) { + case 0: + return wayPoint.getName(); + case 1: + return String.format("%.6f", wayPoint.getLongitude()); + case 2: + return String.format("%.6f", wayPoint.getLatitude()); + case 3: + if (wayPoint.hasElevation()) { + return String.format("%.1f", wayPoint.getElevation()); + } else { + return "-"; + } + case 4: + return wayPoint.getDescription(); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java new file mode 100644 index 000000000..9de1ac73c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/BugReportImporter.java @@ -0,0 +1,89 @@ +/* + * 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 java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +public class BugReportImporter { + + private final static String TAG_HEADER = "------ EVENT LOG TAGS ------"; + private final static String LOG_HEADER = "------ EVENT LOG ------"; + private final static String HEADER_TAG = "------"; + + private String[] mTags; + private String[] mLog; + + public BugReportImporter(String filePath) throws FileNotFoundException { + BufferedReader reader = new BufferedReader( + new InputStreamReader(new FileInputStream(filePath))); + + try { + String line; + while ((line = reader.readLine()) != null) { + if (TAG_HEADER.equals(line)) { + readTags(reader); + return; + } + } + } catch (IOException e) { + } + } + + public String[] getTags() { + return mTags; + } + + public String[] getLog() { + return mLog; + } + + private void readTags(BufferedReader reader) throws IOException { + String line; + + ArrayList content = new ArrayList(); + while ((line = reader.readLine()) != null) { + if (LOG_HEADER.equals(line)) { + mTags = content.toArray(new String[content.size()]); + readLog(reader); + return; + } else { + content.add(line); + } + } + } + + private void readLog(BufferedReader reader) throws IOException { + String line; + + ArrayList content = new ArrayList(); + while ((line = reader.readLine()) != null) { + if (line.startsWith(HEADER_TAG) == false) { + content.add(line); + } else { + break; + } + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java new file mode 100644 index 000000000..473387aa2 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java @@ -0,0 +1,55 @@ +/* + * 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.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; + +import java.util.ArrayList; + +public class DisplayFilteredLog extends DisplayLog { + + public DisplayFilteredLog(String name) { + super(name); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList valueDescriptors = + new ArrayList(); + + ArrayList occurrenceDescriptors = + new ArrayList(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + addToLog(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_FILTERED_LOG; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java new file mode 100644 index 000000000..0cffd7e07 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java @@ -0,0 +1,422 @@ +/* + * 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.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; +import org.jfree.chart.renderer.xy.XYAreaRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.Millisecond; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class DisplayGraph extends EventDisplay { + + public DisplayGraph(String name) { + super(name); + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + Collection datasets = mValueTypeDataSetMap.values(); + for (TimeSeriesCollection dataset : datasets) { + dataset.removeAllSeries(); + } + if (mOccurrenceDataSet != null) { + mOccurrenceDataSet.removeAllSeries(); + } + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + String title = getChartTitle(logParser); + return createCompositeChart(parent, logParser, title); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList valueDescriptors = + new ArrayList(); + + ArrayList occurrenceDescriptors = + new ArrayList(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + updateChart(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined + * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from + * the two lists. + *

This method is only called when at least one of the descriptor list is non empty. + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + private void updateChart(EventContainer event, EventLogParser logParser, + ArrayList valueDescriptors, + ArrayList occurrenceDescriptors) { + Map tagMap = logParser.getTagMap(); + + Millisecond millisecondTime = null; + long msec = -1; + + // If the event container is a cpu container (tag == 2721), and there is no descriptor + // for the total CPU load, then we do accumulate all the values. + boolean accumulateValues = false; + double accumulatedValue = 0; + + if (event.mTag == 2721) { + accumulateValues = true; + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + accumulateValues &= (descriptor.valueIndex != 0); + } + } + + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + try { + // get the hashmap for this descriptor + HashMap map = mValueDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap(); + mValueDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it + if (timeSeries == null) { + // get the series name + String seriesFullName = null; + String seriesLabel = getSeriesLabel(event, descriptor); + + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + seriesFullName = String.format("%1$s / %2$s", seriesLabel, + descriptor.valueName); + break; + case EVENT_CHECK_SAME_VALUE: + seriesFullName = String.format("%1$s", seriesLabel); + break; + default: + seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel, + tagMap.get(descriptor.eventTag), + descriptor.valueName); + break; + } + + // get the data set for this ValueType + TimeSeriesCollection dataset = getValueDataset( + logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex] + .getValueType(), + accumulateValues); + + // create the series + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000); + } + + dataset.addSeries(timeSeries); + + // add it to the map. + map.put(event.pid, timeSeries); + } + + // update the timeSeries. + + // get the value from the event + double value = event.getValueAsDouble(descriptor.valueIndex); + + // accumulate the values if needed. + if (accumulateValues) { + accumulatedValue += value; + value = accumulatedValue; + } + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, value); + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) { + try { + // get the hashmap for this descriptor + HashMap map = mOcurrenceDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap(); + mOcurrenceDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it. + if (timeSeries == null) { + String seriesLabel = getSeriesLabel(event, descriptor); + + String seriesFullName = String.format("[%1$s:%2$s]", + tagMap.get(descriptor.eventTag), seriesLabel); + + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge); + } + + getOccurrenceDataSet().addSeries(timeSeries); + + map.put(event.pid, timeSeries); + } + + // update the series + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + // go through all the series and remove old values. + if (msec != -1 && mMaximumChartItemAge != -1) { + Collection> pidMapValues = + mValueDescriptorSeriesMap.values(); + + for (HashMap pidMapValue : pidMapValues) { + Collection seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + + pidMapValues = mOcurrenceDescriptorSeriesMap.values(); + for (HashMap pidMapValue : pidMapValues) { + Collection seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + } + } + + /** + * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}. + * If the data set is not yet created, it is first allocated and set up into the + * {@link org.jfree.chart.JFreeChart} object. + * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set. + * @param accumulateValues + */ + private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) { + TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type); + if (dataset == null) { + // create the data set and store it in the map + dataset = new TimeSeriesCollection(); + mValueTypeDataSetMap.put(type, dataset); + + // create the renderer and configure it depending on the ValueType + AbstractXYItemRenderer renderer; + if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) { + renderer = new XYAreaRenderer(); + } else { + XYLineAndShapeRenderer r = new XYLineAndShapeRenderer(); + r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT); + + renderer = r; + } + + // set both the dataset and the renderer in the plot object. + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, dataset); + xyPlot.setRenderer(mDataSetCount, renderer); + + // put a new axis label, and configure it. + NumberAxis axis = new NumberAxis(type.toString()); + + if (type == EventValueDescription.ValueType.PERCENT) { + // force percent range to be (0,100) fixed. + axis.setAutoRange(false); + axis.setRange(0., 100.); + } + + // for the index, we ignore the occurrence dataset + int count = mDataSetCount; + if (mOccurrenceDataSet != null) { + count--; + } + + xyPlot.setRangeAxis(count, axis); + if ((count % 2) == 0) { + xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT); + } else { + xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT); + } + + // now we link the dataset and the axis + xyPlot.mapDatasetToRangeAxis(mDataSetCount, count); + + mDataSetCount++; + } + + return dataset; + } + + /** + * Return the series label for this event. This only contains the pid information. + * @param event the {@link EventContainer} + * @param descriptor the {@link OccurrenceDisplayDescriptor} + * @return the series label. + * @throws InvalidTypeException + */ + private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor) + throws InvalidTypeException { + if (descriptor.seriesValueIndex != -1) { + if (descriptor.includePid == false) { + return event.getValueAsString(descriptor.seriesValueIndex); + } else { + return String.format("%1$s (%2$d)", + event.getValueAsString(descriptor.seriesValueIndex), event.pid); + } + } + + return Integer.toString(event.pid); + } + + /** + * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not + * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object. + */ + private TimeSeriesCollection getOccurrenceDataSet() { + if (mOccurrenceDataSet == null) { + mOccurrenceDataSet = new TimeSeriesCollection(); + + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet); + + OccurrenceRenderer renderer = new OccurrenceRenderer(); + renderer.setBaseShapesVisible(false); + xyPlot.setRenderer(mDataSetCount, renderer); + + mDataSetCount++; + } + + return mOccurrenceDataSet; + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_GRAPH; + } + + /** + * Sets the current {@link EventLogParser} object. + */ + @Override + protected void setNewLogParser(EventLogParser logParser) { + if (mChart != null) { + mChart.setTitle(getChartTitle(logParser)); + } + } + /** + * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}. + * + * @param logParser the logParser. + * @return the chart title. + */ + private String getChartTitle(EventLogParser logParser) { + if (mValueDescriptors.size() > 0) { + String chartDesc = null; + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + if (logParser != null) { + chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag); + } + break; + case EVENT_CHECK_SAME_VALUE: + if (logParser != null) { + chartDesc = String.format("%1$s / %2$s", + logParser.getTagMap().get(mValueDescriptors.get(0).eventTag), + mValueDescriptors.get(0).valueName); + } + break; + } + + if (chartDesc != null) { + return String.format("%1$s - %2$s", mName, chartDesc); + } + } + + return mName; + } +} \ No newline at end of file diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java new file mode 100644 index 000000000..26296f31c --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java @@ -0,0 +1,379 @@ +/* + * 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.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.InvalidTypeException; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.TableHelper; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +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.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.Calendar; + +public class DisplayLog extends EventDisplay { + public DisplayLog(String name) { + super(name); + } + + private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$ + private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$ + private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$ + private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$ + private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$ + private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$ + + /** + * Resets the display. + */ + @Override + void resetUI() { + mLogTable.removeAll(); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + addToLog(event, logParser); + } + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + Control createComposite(Composite parent, EventLogParser logParser, ILogColumnListener listener) { + return createLogUI(parent, listener); + } + + /** + * Adds an {@link EventContainer} to the log. + * + * @param event the event. + * @param logParser the log parser. + */ + private void addToLog(EventContainer event, EventLogParser logParser) { + ScrollBar bar = mLogTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // get the date. + Calendar c = Calendar.getInstance(); + long msec = (long) event.sec * 1000L; + c.setTimeInMillis(msec); + + // convert the time into a string + String date = String.format("%1$tF %1$tT", c); + + String eventName = logParser.getTagMap().get(event.mTag); + String pidName = Integer.toString(event.pid); + + // get the value description + EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag); + if (valueDescription != null) { + for (int i = 0; i < valueDescription.length; i++) { + EventValueDescription description = valueDescription[i]; + try { + String value = event.getValueAsString(i); + + logValue(date, pidName, eventName, description.getName(), value, + description.getEventValueType(), description.getValueType()); + } catch (InvalidTypeException e) { + logValue(date, pidName, eventName, description.getName(), e.getMessage(), + description.getEventValueType(), description.getValueType()); + } + } + + // scroll if needed, by showing the last item + if (scroll) { + int itemCount = mLogTable.getItemCount(); + if (itemCount > 0) { + mLogTable.showItem(mLogTable.getItem(itemCount - 1)); + } + } + } + } + + /** + * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by + * the list of descriptors. If an event is configured to be displayed by value and occurrence, + * only the values are displayed (as they mark an event occurrence anyway). + *

This method is only called when at least one of the descriptor list is non empty. + * + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + protected void addToLog(EventContainer event, EventLogParser logParser, + ArrayList valueDescriptors, + ArrayList occurrenceDescriptors) { + ScrollBar bar = mLogTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // get the date. + Calendar c = Calendar.getInstance(); + long msec = (long) event.sec * 1000L; + c.setTimeInMillis(msec); + + // convert the time into a string + String date = String.format("%1$tF %1$tT", c); + + String eventName = logParser.getTagMap().get(event.mTag); + String pidName = Integer.toString(event.pid); + + if (valueDescriptors.size() > 0) { + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + logDescriptor(event, descriptor, date, pidName, eventName, logParser); + } + } else { + // we display the event. Since the StringBuilder contains the header (date, event name, + // pid) at this point, there isn't anything else to display. + } + + // scroll if needed, by showing the last item + if (scroll) { + int itemCount = mLogTable.getItemCount(); + if (itemCount > 0) { + mLogTable.showItem(mLogTable.getItem(itemCount - 1)); + } + } + } + + + /** + * Logs a value in the ui. + * + * @param date + * @param pid + * @param event + * @param valueName + * @param value + * @param eventValueType + * @param valueType + */ + private void logValue(String date, String pid, String event, String valueName, + String value, EventContainer.EventValueType eventValueType, EventValueDescription.ValueType valueType) { + + TableItem item = new TableItem(mLogTable, SWT.NONE); + item.setText(0, date); + item.setText(1, pid); + item.setText(2, event); + item.setText(3, valueName); + item.setText(4, value); + + String type; + if (valueType != EventValueDescription.ValueType.NOT_APPLICABLE) { + type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString()); + } else { + type = eventValueType.toString(); + } + + item.setText(5, type); + } + + /** + * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}. + * + * @param event the EventContainer + * @param descriptor the ValueDisplayDescriptor defining which value to display. + * @param date the date of the event in a string. + * @param pidName + * @param eventName + * @param logParser + */ + private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor, + String date, String pidName, String eventName, EventLogParser logParser) { + + String value; + try { + value = event.getValueAsString(descriptor.valueIndex); + } catch (InvalidTypeException e) { + value = e.getMessage(); + } + + EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag); + + EventValueDescription valueDescription = values[descriptor.valueIndex]; + + logValue(date, pidName, eventName, descriptor.valueName, value, + valueDescription.getEventValueType(), valueDescription.getValueType()); + } + + /** + * Creates the UI for a log display. + * + * @param parent the parent {@link Composite} + * @param listener the {@link ILogColumnListener} to notify on column resize events. + * @return the top Composite of the UI. + */ + private Control createLogUI(Composite parent, final ILogColumnListener listener) { + Composite mainComp = new Composite(parent, SWT.NONE); + GridLayout gl; + mainComp.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + mainComp.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mLogTable = null; + } + }); + + Label l = new Label(mainComp, SWT.CENTER); + l.setText(mName); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL | + SWT.BORDER); + mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + TableColumn col = TableHelper.createTableColumn( + mLogTable, "Time", + SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(0, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "pid", + SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(1, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Event", + SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(2, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Name", + SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(3, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Value", + SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(4, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Type", + SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(5, (TableColumn) source); + } + } + }); + + mLogTable.setHeaderVisible(true); + mLogTable.setLinesVisible(true); + + return mainComp; + } + + /** + * Resizes the index-th column of the log {@link Table} (if applicable). + *

+ * This does nothing if the Table object is null (because the display + * type does not use a column) or if the index-th column is in fact the originating + * column passed as argument. + * + * @param index the index of the column to resize + * @param sourceColumn the original column that was resize, and on which we need to sync the + * index-th column width. + */ + @Override + void resizeColumn(int index, TableColumn sourceColumn) { + if (mLogTable != null) { + TableColumn col = mLogTable.getColumn(index); + if (col != sourceColumn) { + col.setWidth(sourceColumn.getWidth()); + } + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_LOG_ALL; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java new file mode 100644 index 000000000..1c006bcda --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java @@ -0,0 +1,299 @@ +/* + * 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.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.FixedMillisecond; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.util.ShapeUtilities; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class DisplaySync extends SyncCommon { + + // Information to graph for each authority + private TimePeriodValues mDatasetsSync[]; + private List mTooltipsSync[]; + private CustomXYToolTipGenerator mTooltipGenerators[]; + private TimeSeries mDatasetsSyncTickle[]; + + // Dataset of error events to graph + private TimeSeries mDatasetError; + + public DisplaySync(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Status"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + + XYBarRenderer br = new XYBarRenderer(); + mDatasetsSync = new TimePeriodValues[NUM_AUTHS]; + mTooltipsSync = new List[NUM_AUTHS]; + mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS]; + + TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(0, br); + + XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer(); + ls.setBaseLinesVisible(false); + mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS]; + TimeSeriesCollection tsc = new TimeSeriesCollection(); + xyPlot.setDataset(1, tsc); + xyPlot.setRenderer(1, ls); + + mDatasetError = new TimeSeries("Errors", FixedMillisecond.class); + xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError)); + XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer(); + errls.setBaseLinesVisible(false); + errls.setSeriesPaint(0, Color.RED); + xyPlot.setRenderer(2, errls); + + for (int i = 0; i < NUM_AUTHS; i++) { + br.setSeriesPaint(i, AUTH_COLORS[i]); + ls.setSeriesPaint(i, AUTH_COLORS[i]); + mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]); + tpvc.addSeries(mDatasetsSync[i]); + mTooltipsSync[i] = new ArrayList(); + mTooltipGenerators[i] = new CustomXYToolTipGenerator(); + br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]); + mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]); + + mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle", + FixedMillisecond.class); + tsc.addSeries(mDatasetsSyncTickle[i]); + ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f)); + } + } + + /** + * Updates the display with a new event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + super.newEvent(event, logParser); // Handle sync operation + try { + if (event.mTag == EVENT_TICKLE) { + int auth = getAuth(event.getValueAsString(0)); + if (auth >= 0) { + long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1); + } + } + } catch (InvalidTypeException e) { + } + } + + /** + * Generate the height for an event. + * Height is somewhat arbitrarily the count of "things" that happened + * during the sync. + * When network traffic measurements are available, code should be modified + * to use that instead. + * @param details The details string associated with the event + * @return The height in arbirary units (0-100) + */ + private int getHeightFromDetails(String details) { + if (details == null) { + return 1; // Arbitrary + } + int total = 0; + String parts[] = details.split("[a-zA-Z]"); + for (String part : parts) { + if ("".equals(part)) continue; + total += Integer.parseInt(part); + } + if (total == 0) { + total = 1; + } + return total; + } + + /** + * Generates the tooltips text for an event. + * This method decodes the cryptic details string. + * @param auth The authority associated with the event + * @param details The details string + * @param eventSource server, poll, etc. + * @return The text to display in the tooltips + */ + private String getTextFromDetails(int auth, String details, int eventSource) { + + StringBuffer sb = new StringBuffer(); + sb.append(AUTH_NAMES[auth]).append(": \n"); + + Scanner scanner = new Scanner(details); + Pattern charPat = Pattern.compile("[a-zA-Z]"); + Pattern numPat = Pattern.compile("[0-9]+"); + while (scanner.hasNext()) { + String key = scanner.findInLine(charPat); + int val = Integer.parseInt(scanner.findInLine(numPat)); + if (auth == GMAIL && "M".equals(key)) { + sb.append("messages from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "L".equals(key)) { + sb.append("labels from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "C".equals(key)) { + sb.append("check conversation requests from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "A".equals(key)) { + sb.append("attachments from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "U".equals(key)) { + sb.append("op updates from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "u".equals(key)) { + sb.append("op updates to server: ").append(val).append("\n"); + } else if (auth == GMAIL && "S".equals(key)) { + sb.append("send/receive cycles: ").append(val).append("\n"); + } else if ("Q".equals(key)) { + sb.append("queries to server: ").append(val).append("\n"); + } else if ("E".equals(key)) { + sb.append("entries from server: ").append(val).append("\n"); + } else if ("u".equals(key)) { + sb.append("updates from client: ").append(val).append("\n"); + } else if ("i".equals(key)) { + sb.append("inserts from client: ").append(val).append("\n"); + } else if ("d".equals(key)) { + sb.append("deletes from client: ").append(val).append("\n"); + } else if ("f".equals(key)) { + sb.append("full sync requested\n"); + } else if ("r".equals(key)) { + sb.append("partial sync unavailable\n"); + } else if ("X".equals(key)) { + sb.append("hard error\n"); + } else if ("e".equals(key)) { + sb.append("number of parse exceptions: ").append(val).append("\n"); + } else if ("c".equals(key)) { + sb.append("number of conflicts: ").append(val).append("\n"); + } else if ("a".equals(key)) { + sb.append("number of auth exceptions: ").append(val).append("\n"); + } else if ("D".equals(key)) { + sb.append("too many deletions\n"); + } else if ("R".equals(key)) { + sb.append("too many retries: ").append(val).append("\n"); + } else if ("b".equals(key)) { + sb.append("database error\n"); + } else if ("x".equals(key)) { + sb.append("soft error\n"); + } else if ("l".equals(key)) { + sb.append("sync already in progress\n"); + } else if ("I".equals(key)) { + sb.append("io exception\n"); + } else if (auth == CONTACTS && "g".equals(key)) { + sb.append("aggregation query: ").append(val).append("\n"); + } else if (auth == CONTACTS && "G".equals(key)) { + sb.append("aggregation merge: ").append(val).append("\n"); + } else if (auth == CONTACTS && "n".equals(key)) { + sb.append("num entries: ").append(val).append("\n"); + } else if (auth == CONTACTS && "p".equals(key)) { + sb.append("photos uploaded from server: ").append(val).append("\n"); + } else if (auth == CONTACTS && "P".equals(key)) { + sb.append("photos downloaded from server: ").append(val).append("\n"); + } else if (auth == CALENDAR && "F".equals(key)) { + sb.append("server refresh\n"); + } else if (auth == CALENDAR && "s".equals(key)) { + sb.append("server diffs fetched\n"); + } else { + sb.append(key).append("=").append(val); + } + } + if (eventSource == 0) { + sb.append("(server)"); + } else if (eventSource == 1) { + sb.append("(local)"); + } else if (eventSource == 2) { + sb.append("(poll)"); + } else if (eventSource == 3) { + sb.append("(user)"); + } + return sb.toString(); + } + + + /** + * Callback to process a sync event. + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (!newEvent) { + // Details arrived for a previous sync event + // Remove event before reinserting. + int lastItem = mDatasetsSync[auth].getItemCount(); + mDatasetsSync[auth].delete(lastItem-1, lastItem-1); + mTooltipsSync[auth].remove(lastItem-1); + } + double height = getHeightFromDetails(details); + height = height / (stopTime - startTime + 1) * 10000; + if (height > 30) { + height = 30; + } + mDatasetsSync[auth].add(new SimpleTimePeriod(startTime, stopTime), height); + mTooltipsSync[auth].add(getTextFromDetails(auth, details, syncSource)); + mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]); + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java new file mode 100644 index 000000000..36d90ce61 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java @@ -0,0 +1,177 @@ +/* + * 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.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.data.time.RegularTimePeriod; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; + +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +public class DisplaySyncHistogram extends SyncCommon { + + Map mTimePeriodMap[]; + + // Information to graph for each authority + private TimePeriodValues mDatasetsSyncHist[]; + + public DisplaySyncHistogram(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Histogram"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + + AbstractXYItemRenderer br = new XYBarRenderer(); + mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1]; + mTimePeriodMap = new HashMap[NUM_AUTHS + 1]; + + TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(br); + + for (int i = 0; i < NUM_AUTHS + 1; i++) { + br.setSeriesPaint(i, AUTH_COLORS[i]); + mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]); + tpvc.addSeries(mDatasetsSyncHist[i]); + mTimePeriodMap[i] = new HashMap(); + + } + } + + /** + * Callback to process a sync event. + * + * @param event The sync event + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (newEvent) { + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + auth = ERRORS; + } + double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour + addHistEvent(0, auth, delta); + } else { + // sync_details arrived for an event that has already been graphed. + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + // Item turns out to be in error, so transfer time from old auth to error. + double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour + addHistEvent(0, auth, -delta); + addHistEvent(0, ERRORS, delta); + } + } + } + + /** + * Helper to add an event to the data series. + * Also updates error series if appropriate (x or X in details). + * @param stopTime Time event ends + * @param auth Sync authority + * @param value Value to graph for event + */ + private void addHistEvent(long stopTime, int auth, double value) { + SimpleTimePeriod hour = getTimePeriod(stopTime, mHistWidth); + + // Loop over all datasets to do the stacking. + for (int i = auth; i <= ERRORS; i++) { + addToPeriod(mDatasetsSyncHist, i, hour, value); + } + } + + private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period, + double value) { + int index; + if (mTimePeriodMap[auth].containsKey(period)) { + index = mTimePeriodMap[auth].get(period); + double oldValue = tpv[auth].getValue(index).doubleValue(); + tpv[auth].update(index, oldValue + value); + } else { + index = tpv[auth].getItemCount(); + mTimePeriodMap[auth].put(period, index); + tpv[auth].add(period, value); + } + } + + /** + * Creates a multiple-hour time period for the histogram. + * @param time Time in milliseconds. + * @param numHoursWide: should divide into a day. + * @return SimpleTimePeriod covering the number of hours and containing time. + */ + private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) { + Date date = new Date(time); + TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE; + Calendar calendar = Calendar.getInstance(zone); + calendar.setTime(date); + long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) + + calendar.get(Calendar.DAY_OF_YEAR) * 24; + int year = calendar.get(Calendar.YEAR); + hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide; + calendar.clear(); + calendar.set(year, 0, 1, 0, 0); // Jan 1 + long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000; + return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000); + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC_HIST; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java new file mode 100644 index 000000000..9ce704550 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2009 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.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +public class DisplaySyncPerf extends SyncCommon { + + CustomXYToolTipGenerator mTooltipGenerator; + List mTooltips[]; + + // The series number for each graphed item. + // sync authorities are 0-3 + private static final int DB_QUERY = 4; + private static final int DB_WRITE = 5; + private static final int HTTP_NETWORK = 6; + private static final int HTTP_PROCESSING = 7; + private static final int NUM_SERIES = (HTTP_PROCESSING + 1); + private static final String SERIES_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", + "DB Query", "DB Write", "HTTP Response", "HTTP Processing",}; + private static final Color SERIES_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, + Color.ORANGE, Color.RED, Color.CYAN, Color.PINK, Color.DARK_GRAY}; + private static final double SERIES_YCOORD[] = {0, 0, 0, 0, 1, 1, 2, 2}; + + // Values from data/etc/event-log-tags + private static final int EVENT_DB_OPERATION = 52000; + private static final int EVENT_HTTP_STATS = 52001; + // op types for EVENT_DB_OPERATION + final int EVENT_DB_QUERY = 0; + final int EVENT_DB_WRITE = 1; + + // Information to graph for each authority + private TimePeriodValues mDatasets[]; + + /** + * TimePeriodValuesCollection that supports Y intervals. This allows the + * creation of "floating" bars, rather than bars rooted to the axis. + */ + class YIntervalTimePeriodValuesCollection extends TimePeriodValuesCollection { + /** default serial UID */ + private static final long serialVersionUID = 1L; + + private double yheight; + + /** + * Constructs a collection of bars with a fixed Y height. + * + * @param yheight The height of the bars. + */ + YIntervalTimePeriodValuesCollection(double yheight) { + this.yheight = yheight; + } + + /** + * Returns ending Y value that is a fixed amount greater than the starting value. + * + * @param series the series (zero-based index). + * @param item the item (zero-based index). + * @return The ending Y value for the specified series and item. + */ + @Override + public Number getEndY(int series, int item) { + return getY(series, item).doubleValue() + yheight; + } + } + + /** + * Constructs a graph of network and database stats. + * + * @param name The name of this graph in the graph list. + */ + public DisplaySyncPerf(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Performance"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.getRangeAxis().setVisible(false); + mTooltipGenerator = new CustomXYToolTipGenerator(); + mTooltips = new List[NUM_SERIES]; + + XYBarRenderer br = new XYBarRenderer(); + br.setUseYInterval(true); + mDatasets = new TimePeriodValues[NUM_SERIES]; + + TimePeriodValuesCollection tpvc = new YIntervalTimePeriodValuesCollection(1); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(br); + + for (int i = 0; i < NUM_SERIES; i++) { + br.setSeriesPaint(i, SERIES_COLORS[i]); + mDatasets[i] = new TimePeriodValues(SERIES_NAMES[i]); + tpvc.addSeries(mDatasets[i]); + mTooltips[i] = new ArrayList(); + mTooltipGenerator.addToolTipSeries(mTooltips[i]); + br.setSeriesToolTipGenerator(i, mTooltipGenerator); + } + } + + /** + * Updates the display with a new event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + super.newEvent(event, logParser); // Handle sync operation + try { + if (event.mTag == EVENT_DB_OPERATION) { + // 52000 db_operation (name|3),(op_type|1|5),(time|2|3) + String tip = event.getValueAsString(0); + long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + int opType = Integer.parseInt(event.getValueAsString(1)); + long duration = Long.parseLong(event.getValueAsString(2)); + + if (opType == EVENT_DB_QUERY) { + mDatasets[DB_QUERY].add(new SimpleTimePeriod(endTime - duration, endTime), + SERIES_YCOORD[DB_QUERY]); + mTooltips[DB_QUERY].add(tip); + } else if (opType == EVENT_DB_WRITE) { + mDatasets[DB_WRITE].add(new SimpleTimePeriod(endTime - duration, endTime), + SERIES_YCOORD[DB_WRITE]); + mTooltips[DB_WRITE].add(tip); + } + } else if (event.mTag == EVENT_HTTP_STATS) { + // 52001 http_stats (useragent|3),(response|2|3),(processing|2|3),(tx|1|2),(rx|1|2) + String tip = event.getValueAsString(0) + ", tx:" + event.getValueAsString(3) + + ", rx: " + event.getValueAsString(4); + long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + long netEndTime = endTime - Long.parseLong(event.getValueAsString(2)); + long netStartTime = netEndTime - Long.parseLong(event.getValueAsString(1)); + mDatasets[HTTP_NETWORK].add(new SimpleTimePeriod(netStartTime, netEndTime), + SERIES_YCOORD[HTTP_NETWORK]); + mDatasets[HTTP_PROCESSING].add(new SimpleTimePeriod(netEndTime, endTime), + SERIES_YCOORD[HTTP_PROCESSING]); + mTooltips[HTTP_NETWORK].add(tip); + mTooltips[HTTP_PROCESSING].add(tip); + } + } catch (InvalidTypeException e) { + } + } + + /** + * Callback from super.newEvent to process a sync event. + * + * @param event The sync event + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (newEvent) { + mDatasets[auth].add(new SimpleTimePeriod(startTime, stopTime), SERIES_YCOORD[auth]); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC_PERF; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java new file mode 100644 index 000000000..2223a4d70 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java @@ -0,0 +1,971 @@ +/* + * 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.Log; +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventContainer.CompareMethod; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription.ValueType; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.event.ChartChangeEvent; +import org.jfree.chart.event.ChartChangeEventType; +import org.jfree.chart.event.ChartChangeListener; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.time.Millisecond; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.experimental.chart.swt.ChartComposite; +import org.jfree.experimental.swt.SWTUtils; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Represents a custom display of one or more events. + */ +abstract class EventDisplay { + + private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$ + private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$ + private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$ + private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$ + + private final static String FILTER_VALUE_NULL = ""; //$NON-NLS-1$ + + public final static int DISPLAY_TYPE_LOG_ALL = 0; + public final static int DISPLAY_TYPE_FILTERED_LOG = 1; + public final static int DISPLAY_TYPE_GRAPH = 2; + public final static int DISPLAY_TYPE_SYNC = 3; + public final static int DISPLAY_TYPE_SYNC_HIST = 4; + public final static int DISPLAY_TYPE_SYNC_PERF = 5; + + private final static int EVENT_CHECK_FAILED = 0; + protected final static int EVENT_CHECK_SAME_TAG = 1; + protected final static int EVENT_CHECK_SAME_VALUE = 2; + + /** + * Creates the appropriate EventDisplay subclass. + * + * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc) + * @param name the name of the display + * @return the created object + */ + public static EventDisplay eventDisplayFactory(int type, String name) { + switch (type) { + case DISPLAY_TYPE_LOG_ALL: + return new DisplayLog(name); + case DISPLAY_TYPE_FILTERED_LOG: + return new DisplayFilteredLog(name); + case DISPLAY_TYPE_SYNC: + return new DisplaySync(name); + case DISPLAY_TYPE_SYNC_HIST: + return new DisplaySyncHistogram(name); + case DISPLAY_TYPE_GRAPH: + return new DisplayGraph(name); + case DISPLAY_TYPE_SYNC_PERF: + return new DisplaySyncPerf(name); + default: + throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$ + } + } + + /** + * Adds event to the display. + * @param event The event + * @param logParser The log parser. + */ + abstract void newEvent(EventContainer event, EventLogParser logParser); + + /** + * Resets the display. + */ + abstract void resetUI(); + + /** + * Gets display type + * + * @return display type as an integer + */ + abstract int getDisplayType(); + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + abstract Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener); + + interface ILogColumnListener { + void columnResized(int index, TableColumn sourceColumn); + } + + /** + * Describes an event to be displayed. + */ + static class OccurrenceDisplayDescriptor { + + int eventTag = -1; + int seriesValueIndex = -1; + boolean includePid = false; + int filterValueIndex = -1; + CompareMethod filterCompareMethod = CompareMethod.EQUAL_TO; + Object filterValue = null; + + OccurrenceDisplayDescriptor() { + } + + OccurrenceDisplayDescriptor(OccurrenceDisplayDescriptor descriptor) { + replaceWith(descriptor); + } + + OccurrenceDisplayDescriptor(int eventTag) { + this.eventTag = eventTag; + } + + OccurrenceDisplayDescriptor(int eventTag, int seriesValueIndex) { + this.eventTag = eventTag; + this.seriesValueIndex = seriesValueIndex; + } + + void replaceWith(OccurrenceDisplayDescriptor descriptor) { + eventTag = descriptor.eventTag; + seriesValueIndex = descriptor.seriesValueIndex; + includePid = descriptor.includePid; + filterValueIndex = descriptor.filterValueIndex; + filterCompareMethod = descriptor.filterCompareMethod; + filterValue = descriptor.filterValue; + } + + /** + * Loads the descriptor parameter from a storage string. The storage string must have + * been generated with {@link #getStorageString()}. + * + * @param storageString the storage string + */ + final void loadFrom(String storageString) { + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR)); + loadFrom(values, 0); + } + + /** + * Loads the parameters from an array of strings. + * + * @param storageStrings the strings representing each parameter. + * @param index the starting index in the array of strings. + * @return the new index in the array. + */ + protected int loadFrom(String[] storageStrings, int index) { + eventTag = Integer.parseInt(storageStrings[index++]); + seriesValueIndex = Integer.parseInt(storageStrings[index++]); + includePid = Boolean.parseBoolean(storageStrings[index++]); + filterValueIndex = Integer.parseInt(storageStrings[index++]); + try { + filterCompareMethod = CompareMethod.valueOf(storageStrings[index++]); + } catch (IllegalArgumentException e) { + // if the name does not match any known CompareMethod, we init it to the default one + filterCompareMethod = CompareMethod.EQUAL_TO; + } + String value = storageStrings[index++]; + if (filterValueIndex != -1 && FILTER_VALUE_NULL.equals(value) == false) { + filterValue = EventValueType.getObjectFromStorageString(value); + } + + return index; + } + + /** + * Returns the storage string for the receiver. + */ + String getStorageString() { + StringBuilder sb = new StringBuilder(); + sb.append(eventTag); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(seriesValueIndex); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(Boolean.toString(includePid)); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(filterValueIndex); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(filterCompareMethod.name()); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + if (filterValue != null) { + String value = EventValueType.getStorageString(filterValue); + if (value != null) { + sb.append(value); + } else { + sb.append(FILTER_VALUE_NULL); + } + } else { + sb.append(FILTER_VALUE_NULL); + } + + return sb.toString(); + } + } + + /** + * Describes an event value to be displayed. + */ + static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor { + String valueName; + int valueIndex = -1; + + ValueDisplayDescriptor() { + super(); + } + + ValueDisplayDescriptor(ValueDisplayDescriptor descriptor) { + super(); + replaceWith(descriptor); + } + + ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex) { + super(eventTag); + this.valueName = valueName; + this.valueIndex = valueIndex; + } + + ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex, + int seriesValueIndex) { + super(eventTag, seriesValueIndex); + this.valueName = valueName; + this.valueIndex = valueIndex; + } + + @Override + void replaceWith(OccurrenceDisplayDescriptor descriptor) { + super.replaceWith(descriptor); + if (descriptor instanceof ValueDisplayDescriptor) { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor; + valueName = valueDescriptor.valueName; + valueIndex = valueDescriptor.valueIndex; + } + } + + /** + * Loads the parameters from an array of strings. + * + * @param storageStrings the strings representing each parameter. + * @param index the starting index in the array of strings. + * @return the new index in the array. + */ + @Override + protected int loadFrom(String[] storageStrings, int index) { + index = super.loadFrom(storageStrings, index); + valueName = storageStrings[index++]; + valueIndex = Integer.parseInt(storageStrings[index++]); + return index; + } + + /** + * Returns the storage string for the receiver. + */ + @Override + String getStorageString() { + String superStorage = super.getStorageString(); + + StringBuilder sb = new StringBuilder(); + sb.append(superStorage); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(valueName); + sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); + sb.append(valueIndex); + + return sb.toString(); + } + } + + /* ================== + * Event Display parameters. + * ================== */ + protected String mName; + + private boolean mPidFiltering = false; + + private ArrayList mPidFilterList = null; + + protected final ArrayList mValueDescriptors = + new ArrayList(); + private final ArrayList mOccurrenceDescriptors = + new ArrayList(); + + /* ================== + * Event Display members for display purpose. + * ================== */ + // chart objects + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap> mValueDescriptorSeriesMap = + new HashMap>(); + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap> mOcurrenceDescriptorSeriesMap = + new HashMap>(); + + /** + * This is a map of (ValueType, dataset) + */ + protected final HashMap mValueTypeDataSetMap = + new HashMap(); + + protected JFreeChart mChart; + protected TimeSeriesCollection mOccurrenceDataSet; + protected int mDataSetCount; + private ChartComposite mChartComposite; + protected long mMaximumChartItemAge = -1; + protected long mHistWidth = 1; + + // log objects. + protected Table mLogTable; + + /* ================== + * Misc data. + * ================== */ + protected int mValueDescriptorCheck = EVENT_CHECK_FAILED; + + EventDisplay(String name) { + mName = name; + } + + static EventDisplay clone(EventDisplay from) { + EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName()); + ed.mName = from.mName; + ed.mPidFiltering = from.mPidFiltering; + ed.mMaximumChartItemAge = from.mMaximumChartItemAge; + ed.mHistWidth = from.mHistWidth; + + if (from.mPidFilterList != null) { + ed.mPidFilterList = new ArrayList(); + ed.mPidFilterList.addAll(from.mPidFilterList); + } + + for (ValueDisplayDescriptor desc : from.mValueDescriptors) { + ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc)); + } + ed.mValueDescriptorCheck = from.mValueDescriptorCheck; + + for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) { + ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc)); + } + return ed; + } + + /** + * Returns the parameters of the receiver as a single String for storage. + */ + String getStorageString() { + StringBuilder sb = new StringBuilder(); + + sb.append(mName); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDisplayType()); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(Boolean.toString(mPidFiltering)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getPidStorageString()); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDescriptorStorageString(mValueDescriptors)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(getDescriptorStorageString(mOccurrenceDescriptors)); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(mMaximumChartItemAge); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + sb.append(mHistWidth); + sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); + + return sb.toString(); + } + + void setName(String name) { + mName = name; + } + + String getName() { + return mName; + } + + void setPidFiltering(boolean filterByPid) { + mPidFiltering = filterByPid; + } + + boolean getPidFiltering() { + return mPidFiltering; + } + + void setPidFilterList(ArrayList pids) { + if (mPidFiltering == false) { + new InvalidParameterException(); + } + + mPidFilterList = pids; + } + + ArrayList getPidFilterList() { + return mPidFilterList; + } + + void addPidFiler(int pid) { + if (mPidFiltering == false) { + new InvalidParameterException(); + } + + if (mPidFilterList == null) { + mPidFilterList = new ArrayList(); + } + + mPidFilterList.add(pid); + } + + /** + * Returns an iterator to the list of {@link ValueDisplayDescriptor}. + */ + Iterator getValueDescriptors() { + return mValueDescriptors.iterator(); + } + + /** + * Update checks on the descriptors. Must be called whenever a descriptor is modified outside + * of this class. + */ + void updateValueDescriptorCheck() { + mValueDescriptorCheck = checkDescriptors(); + } + + /** + * Returns an iterator to the list of {@link OccurrenceDisplayDescriptor}. + */ + Iterator getOccurrenceDescriptors() { + return mOccurrenceDescriptors.iterator(); + } + + /** + * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a + * {@link ValueDisplayDescriptor}. + * + * @param descriptor the descriptor to be added. + */ + void addDescriptor(OccurrenceDisplayDescriptor descriptor) { + if (descriptor instanceof ValueDisplayDescriptor) { + mValueDescriptors.add((ValueDisplayDescriptor) descriptor); + mValueDescriptorCheck = checkDescriptors(); + } else { + mOccurrenceDescriptors.add(descriptor); + } + } + + /** + * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}). + * + * @param descriptorClass the class of the descriptor to return. + * @param index the index of the descriptor to return. + * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor} + * or null if descriptorClass is another class. + */ + OccurrenceDisplayDescriptor getDescriptor( + Class descriptorClass, int index) { + + if (descriptorClass == OccurrenceDisplayDescriptor.class) { + return mOccurrenceDescriptors.get(index); + } else if (descriptorClass == ValueDisplayDescriptor.class) { + return mValueDescriptors.get(index); + } + + return null; + } + + /** + * Removes a descriptor based on its class and index. + * + * @param descriptorClass the class of the descriptor. + * @param index the index of the descriptor to be removed. + */ + void removeDescriptor(Class descriptorClass, int index) { + if (descriptorClass == OccurrenceDisplayDescriptor.class) { + mOccurrenceDescriptors.remove(index); + } else if (descriptorClass == ValueDisplayDescriptor.class) { + mValueDescriptors.remove(index); + mValueDescriptorCheck = checkDescriptors(); + } + } + + Control createCompositeChart(final Composite parent, EventLogParser logParser, + String title) { + mChart = ChartFactory.createTimeSeriesChart( + null, + null /* timeAxisLabel */, + null /* valueAxisLabel */, + null, /* dataset. set below */ + true /* legend */, + false /* tooltips */, + false /* urls */); + + // get the font to make a proper title. We need to convert the swt font, + // into an awt font. + Font f = parent.getFont(); + FontData[] fData = f.getFontData(); + + // event though on Mac OS there could be more than one fontData, we'll only use + // the first one. + FontData firstFontData = fData[0]; + + java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(), + firstFontData, true /* ensureSameSize */); + + + mChart.setTitle(new TextTitle(title, awtFont)); + + final XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setRangeCrosshairVisible(true); + xyPlot.setRangeCrosshairLockedOnData(true); + xyPlot.setDomainCrosshairVisible(true); + xyPlot.setDomainCrosshairLockedOnData(true); + + mChart.addChangeListener(new ChartChangeListener() { + public void chartChanged(ChartChangeEvent event) { + ChartChangeEventType type = event.getType(); + if (type == ChartChangeEventType.GENERAL) { + // because the value we need (rangeCrosshair and domainCrosshair) are + // updated on the draw, but the notification happens before the draw, + // we process the click in a future runnable! + parent.getDisplay().asyncExec(new Runnable() { + public void run() { + processClick(xyPlot); + } + }); + } + } + }); + + mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, // max draw width. We don't want it to zoom, so we put a big number + 3000, // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + true, // zoom + true); // tooltips + + mChartComposite.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mValueTypeDataSetMap.clear(); + mDataSetCount = 0; + mOccurrenceDataSet = null; + mChart = null; + mChartComposite = null; + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + }); + + return mChartComposite; + + } + + private void processClick(XYPlot xyPlot) { + double rangeValue = xyPlot.getRangeCrosshairValue(); + if (rangeValue != 0) { + double domainValue = xyPlot.getDomainCrosshairValue(); + + Millisecond msec = new Millisecond(new Date((long) domainValue)); + + // look for values in the dataset that contains data at this TimePeriod + Set descKeys = mValueDescriptorSeriesMap.keySet(); + + for (ValueDisplayDescriptor descKey : descKeys) { + HashMap map = mValueDescriptorSeriesMap.get(descKey); + + Set pidKeys = map.keySet(); + + for (Integer pidKey : pidKeys) { + TimeSeries series = map.get(pidKey); + + Number value = series.getValue(msec); + if (value != null) { + // found a match. lets check against the actual value. + if (value.doubleValue() == rangeValue) { + + return; + } + } + } + } + } + } + + + /** + * Resizes the index-th column of the log {@link Table} (if applicable). + * Subclasses can override if necessary. + *

+ * This does nothing if the Table object is null (because the display + * type does not use a column) or if the index-th column is in fact the originating + * column passed as argument. + * + * @param index the index of the column to resize + * @param sourceColumn the original column that was resize, and on which we need to sync the + * index-th column width. + */ + void resizeColumn(int index, TableColumn sourceColumn) { + } + + /** + * Sets the current {@link EventLogParser} object. + * Subclasses can override if necessary. + */ + protected void setNewLogParser(EventLogParser logParser) { + } + + /** + * Prepares the {@link EventDisplay} for a multi event display. + */ + void startMultiEventDisplay() { + if (mLogTable != null) { + mLogTable.setRedraw(false); + } + } + + /** + * Finalizes the {@link EventDisplay} after a multi event display. + */ + void endMultiEventDisplay() { + if (mLogTable != null) { + mLogTable.setRedraw(true); + } + } + + /** + * Returns the {@link Table} object used to display events, if any. + * + * @return a Table object or null. + */ + Table getTable() { + return mLogTable; + } + + /** + * Loads a new {@link EventDisplay} from a storage string. The string must have been created + * with {@link #getStorageString()}. + * + * @param storageString the storage string + * @return a new {@link EventDisplay} or null if the load failed. + */ + static EventDisplay load(String storageString) { + if (storageString.length() > 0) { + // the storage string is separated by ':' + String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR)); + + try { + int index = 0; + + String name = values[index++]; + int displayType = Integer.parseInt(values[index++]); + boolean pidFiltering = Boolean.parseBoolean(values[index++]); + + EventDisplay ed = eventDisplayFactory(displayType, name); + ed.setPidFiltering(pidFiltering); + + // because empty sections are removed by String.split(), we have to check + // the index for those. + if (index < values.length) { + ed.loadPidFilters(values[index++]); + } + + if (index < values.length) { + ed.loadValueDescriptors(values[index++]); + } + + if (index < values.length) { + ed.loadOccurrenceDescriptors(values[index++]); + } + + ed.updateValueDescriptorCheck(); + + if (index < values.length) { + ed.mMaximumChartItemAge = Long.parseLong(values[index++]); + } + + if (index < values.length) { + ed.mHistWidth = Long.parseLong(values[index++]); + } + + return ed; + } catch (RuntimeException re) { + // we'll return null below. + Log.e("ddms", re); + } + } + + return null; + } + + private String getPidStorageString() { + if (mPidFilterList != null) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Integer i : mPidFilterList) { + if (first == false) { + sb.append(PID_STORAGE_SEPARATOR); + } else { + first = false; + } + sb.append(i); + } + + return sb.toString(); + } + return ""; //$NON-NLS-1$ + } + + + private void loadPidFilters(String storageString) { + if (storageString.length() > 0) { + String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR)); + + for (String value : values) { + if (mPidFilterList == null) { + mPidFilterList = new ArrayList(); + } + mPidFilterList.add(Integer.parseInt(value)); + } + } + } + + private String getDescriptorStorageString( + ArrayList descriptorList) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + + for (OccurrenceDisplayDescriptor descriptor : descriptorList) { + if (first == false) { + sb.append(DESCRIPTOR_STORAGE_SEPARATOR); + } else { + first = false; + } + sb.append(descriptor.getStorageString()); + } + + return sb.toString(); + } + + private void loadOccurrenceDescriptors(String storageString) { + if (storageString.length() == 0) { + return; + } + + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); + + for (String value : values) { + OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor(); + desc.loadFrom(value); + mOccurrenceDescriptors.add(desc); + } + } + + private void loadValueDescriptors(String storageString) { + if (storageString.length() == 0) { + return; + } + + String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); + + for (String value : values) { + ValueDisplayDescriptor desc = new ValueDisplayDescriptor(); + desc.loadFrom(value); + mValueDescriptors.add(desc); + } + } + + /** + * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another + * list if they are configured to display the {@link EventContainer} + * + * @param event the event container + * @param fullList the list with all the descriptors. + * @param outList the list to fill. + */ + @SuppressWarnings("unchecked") + private void getDescriptors(EventContainer event, + ArrayList fullList, + ArrayList outList) { + for (OccurrenceDisplayDescriptor descriptor : fullList) { + try { + // first check the event tag. + if (descriptor.eventTag == event.mTag) { + // now check if we have a filter on a value + if (descriptor.filterValueIndex == -1 || + event.testValue(descriptor.filterValueIndex, descriptor.filterValue, + descriptor.filterCompareMethod)) { + outList.add(descriptor); + } + } + } catch (InvalidTypeException ite) { + // if the filter for the descriptor was incorrect, we ignore the descriptor. + } catch (ArrayIndexOutOfBoundsException aioobe) { + // if the index was wrong (the event content may have changed since we setup the + // display), we do nothing but log the error + Log.e("Event Log", String.format( + "ArrayIndexOutOfBoundsException occured when checking %1$d-th value of event %2$d", //$NON-NLS-1$ + descriptor.filterValueIndex, descriptor.eventTag)); + } + } + } + + /** + * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor} + * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event. + * + * @param event + * @param valueDescriptors + * @param occurrenceDescriptors + * @return true if the event should be displayed. + */ + + protected boolean filterEvent(EventContainer event, + ArrayList valueDescriptors, + ArrayList occurrenceDescriptors) { + + // test the pid first (if needed) + if (mPidFiltering && mPidFilterList != null) { + boolean found = false; + for (int pid : mPidFilterList) { + if (pid == event.pid) { + found = true; + break; + } + } + + if (found == false) { + return false; + } + } + + // now get the list of matching descriptors + getDescriptors(event, mValueDescriptors, valueDescriptors); + getDescriptors(event, mOccurrenceDescriptors, occurrenceDescriptors); + + // and return whether there is at least one match in either list. + return (valueDescriptors.size() > 0 || occurrenceDescriptors.size() > 0); + } + + /** + * Checks all the {@link ValueDisplayDescriptor} for similarity. + * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG. + * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE + * + * @return flag as described above + */ + private int checkDescriptors() { + if (mValueDescriptors.size() < 2) { + return EVENT_CHECK_SAME_VALUE; + } + + int tag = -1; + int index = -1; + for (ValueDisplayDescriptor display : mValueDescriptors) { + if (tag == -1) { + tag = display.eventTag; + index = display.valueIndex; + } else { + if (tag != display.eventTag) { + return EVENT_CHECK_FAILED; + } else { + if (index != -1) { + if (index != display.valueIndex) { + index = -1; + } + } + } + } + } + + if (index == -1) { + return EVENT_CHECK_SAME_TAG; + } + + return EVENT_CHECK_SAME_VALUE; + } + + /** + * Resets the time limit on the chart to be infinite. + */ + void resetChartTimeLimit() { + mMaximumChartItemAge = -1; + } + + /** + * Sets the time limit on the charts. + * + * @param timeLimit the time limit in seconds. + */ + void setChartTimeLimit(long timeLimit) { + mMaximumChartItemAge = timeLimit; + } + + long getChartTimeLimit() { + return mMaximumChartItemAge; + } + + /** + * m + * Resets the histogram width + */ + void resetHistWidth() { + mHistWidth = 1; + } + + /** + * Sets the histogram width + * + * @param histWidth the width in hours + */ + void setHistWidth(long histWidth) { + mHistWidth = histWidth; + } + + long getHistWidth() { + return mHistWidth; + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java new file mode 100644 index 000000000..88c3cb299 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java @@ -0,0 +1,955 @@ +/* + * 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.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; +import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.List; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; + +class EventDisplayOptions extends Dialog { + private static final int DLG_WIDTH = 700; + private static final int DLG_HEIGHT = 700; + + private IImageLoader mImageLoader; + + private Shell mParent; + private Shell mShell; + + private boolean mEditStatus = false; + private final ArrayList mDisplayList = new ArrayList(); + + /* LEFT LIST */ + private List mEventDisplayList; + private Button mEventDisplayNewButton; + private Button mEventDisplayDeleteButton; + private Button mEventDisplayUpButton; + private Button mEventDisplayDownButton; + private Text mDisplayWidthText; + private Text mDisplayHeightText; + + /* WIDGETS ON THE RIGHT */ + private Text mDisplayNameText; + private Combo mDisplayTypeCombo; + private Group mChartOptions; + private Group mHistOptions; + private Button mPidFilterCheckBox; + private Text mPidText; + + /** Map with (event-tag, event name) */ + private Map mEventTagMap; + + /** Map with (event-tag, array of value info for the event) */ + private Map mEventDescriptionMap; + + /** list of current pids */ + private ArrayList mPidList; + + private EventLogParser mLogParser; + + private Group mInfoGroup; + + private static class SelectionWidgets { + private List mList; + private Button mNewButton; + private Button mEditButton; + private Button mDeleteButton; + + private void setEnabled(boolean enable) { + mList.setEnabled(enable); + mNewButton.setEnabled(enable); + mEditButton.setEnabled(enable); + mDeleteButton.setEnabled(enable); + } + } + + private SelectionWidgets mValueSelection; + private SelectionWidgets mOccurrenceSelection; + + /** flag to temporarly disable processing of {@link Text} changes, so that + * {@link Text#setText(String)} can be called safely. */ + private boolean mProcessTextChanges = true; + private Text mTimeLimitText; + private Text mHistWidthText; + + EventDisplayOptions(IImageLoader imageLoader, Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageLoader = imageLoader; + } + + /** + * Opens the display option dialog, to edit the {@link EventDisplay} objects provided in the + * list. + * @param logParser + * @param displayList + * @param eventList + * @return true if the list of {@link EventDisplay} objects was updated. + */ + boolean open(EventLogParser logParser, ArrayList displayList, + ArrayList eventList) { + mLogParser = logParser; + + if (logParser != null) { + // we need 2 things from the parser. + // the event tag / event name map + mEventTagMap = logParser.getTagMap(); + + // the event info map + mEventDescriptionMap = logParser.getEventInfoMap(); + } + + // make a copy of the EventDisplay list since we'll use working copies. + duplicateEventDisplay(displayList); + + // build a list of pid from the list of events. + buildPidList(eventList); + + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + // Set the dialog size. + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.layout(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mEditStatus; + } + + ArrayList getEventDisplays() { + return mDisplayList; + } + + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Event Display Configuration"); + + mShell.setLayout(new GridLayout(1, true)); + + final Composite topPanel = new Composite(mShell, SWT.NONE); + topPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + topPanel.setLayout(new GridLayout(2, false)); + + // create the tree on the left and the controls on the right. + Composite leftPanel = new Composite(topPanel, SWT.NONE); + Composite rightPanel = new Composite(topPanel, SWT.NONE); + + createLeftPanel(leftPanel); + createRightPanel(rightPanel); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + event.doit = true; + } + }); + + Label separator = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Composite bottomButtons = new Composite(mShell, SWT.NONE); + bottomButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + GridLayout gl; + bottomButtons.setLayout(gl = new GridLayout(2, true)); + gl.marginHeight = gl.marginWidth = 0; + + Button okButton = new Button(bottomButtons, SWT.PUSH); + okButton.setText("OK"); + okButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + Button cancelButton = new Button(bottomButtons, SWT.PUSH); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + // cancel the modification flag. + mEditStatus = false; + + // and close + mShell.close(); + } + }); + + enable(false); + + // fill the list with the current display + fillEventDisplayList(); + } + + private void createLeftPanel(Composite leftPanel) { + final IPreferenceStore store = DdmUiPreferences.getStore(); + + GridLayout gl; + + leftPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + leftPanel.setLayout(gl = new GridLayout(1, false)); + gl.verticalSpacing = 1; + + mEventDisplayList = new List(leftPanel, + SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.FULL_SELECTION); + mEventDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH)); + mEventDisplayList.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + handleEventDisplaySelection(); + } + }); + + Composite bottomControls = new Composite(leftPanel, SWT.NONE); + bottomControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + bottomControls.setLayout(gl = new GridLayout(5, false)); + gl.marginHeight = gl.marginWidth = 0; + gl.verticalSpacing = 0; + gl.horizontalSpacing = 0; + + mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayNewButton.setImage(mImageLoader.loadImage("add.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayNewButton.setToolTipText("Adds a new event display"); + mEventDisplayNewButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + mEventDisplayNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + createNewEventDisplay(); + } + }); + + mEventDisplayDeleteButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayDeleteButton.setImage(mImageLoader.loadImage("delete.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayDeleteButton.setToolTipText("Deletes the selected event display"); + mEventDisplayDeleteButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + mEventDisplayDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + deleteEventDisplay(); + } + }); + + mEventDisplayUpButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayUpButton.setImage(mImageLoader.loadImage("up.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayUpButton.setToolTipText("Moves the selected event display up"); + mEventDisplayUpButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get current selection. + int selection = mEventDisplayList.getSelectionIndex(); + if (selection > 0) { + // update the list of EventDisplay. + EventDisplay display = mDisplayList.remove(selection); + mDisplayList.add(selection - 1, display); + + // update the list widget + mEventDisplayList.remove(selection); + mEventDisplayList.add(display.getName(), selection - 1); + + // update the selection and reset the ui. + mEventDisplayList.select(selection - 1); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + } + }); + + mEventDisplayDownButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayDownButton.setImage(mImageLoader.loadImage("down.png", // $NON-NLS-1$ + leftPanel.getDisplay())); + mEventDisplayDownButton.setToolTipText("Moves the selected event display down"); + mEventDisplayDownButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get current selection. + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1 && selection < mEventDisplayList.getItemCount() - 1) { + // update the list of EventDisplay. + EventDisplay display = mDisplayList.remove(selection); + mDisplayList.add(selection + 1, display); + + // update the list widget + mEventDisplayList.remove(selection); + mEventDisplayList.add(display.getName(), selection + 1); + + // update the selection and reset the ui. + mEventDisplayList.select(selection + 1); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + } + }); + + Group sizeGroup = new Group(leftPanel, SWT.NONE); + sizeGroup.setText("Display Size:"); + sizeGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + sizeGroup.setLayout(new GridLayout(2, false)); + + Label l = new Label(sizeGroup, SWT.NONE); + l.setText("Width:"); + + mDisplayWidthText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER); + mDisplayWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayWidthText.setText(Integer.toString( + store.getInt(EventLogPanel.PREFS_DISPLAY_WIDTH))); + mDisplayWidthText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + String text = mDisplayWidthText.getText().trim(); + try { + store.setValue(EventLogPanel.PREFS_DISPLAY_WIDTH, Integer.parseInt(text)); + setModified(); + } catch (NumberFormatException nfe) { + // do something? + } + } + }); + + l = new Label(sizeGroup, SWT.NONE); + l.setText("Height:"); + + mDisplayHeightText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER); + mDisplayHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayHeightText.setText(Integer.toString( + store.getInt(EventLogPanel.PREFS_DISPLAY_HEIGHT))); + mDisplayHeightText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + String text = mDisplayHeightText.getText().trim(); + try { + store.setValue(EventLogPanel.PREFS_DISPLAY_HEIGHT, Integer.parseInt(text)); + setModified(); + } catch (NumberFormatException nfe) { + // do something? + } + } + }); + } + + private void createRightPanel(Composite rightPanel) { + rightPanel.setLayout(new GridLayout(1, true)); + rightPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mInfoGroup = new Group(rightPanel, SWT.NONE); + mInfoGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mInfoGroup.setLayout(new GridLayout(2, false)); + + Label nameLabel = new Label(mInfoGroup, SWT.LEFT); + nameLabel.setText("Name:"); + + mDisplayNameText = new Text(mInfoGroup, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + mDisplayNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDisplayNameText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mProcessTextChanges) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + eventDisplay.setName(mDisplayNameText.getText()); + int index = mEventDisplayList.getSelectionIndex(); + mEventDisplayList.remove(index); + mEventDisplayList.add(eventDisplay.getName(), index); + mEventDisplayList.select(index); + handleEventDisplaySelection(); + setModified(); + } + } + } + }); + + Label displayLabel = new Label(mInfoGroup, SWT.LEFT); + displayLabel.setText("Type:"); + + mDisplayTypeCombo = new Combo(mInfoGroup, SWT.READ_ONLY | SWT.DROP_DOWN); + mDisplayTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + // add the combo values. This must match the values EventDisplay.DISPLAY_TYPE_* + mDisplayTypeCombo.add("Log All"); + mDisplayTypeCombo.add("Filtered Log"); + mDisplayTypeCombo.add("Graph"); + mDisplayTypeCombo.add("Sync"); + mDisplayTypeCombo.add("Sync Histogram"); + mDisplayTypeCombo.add("Sync Performance"); + mDisplayTypeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null && eventDisplay.getDisplayType() != mDisplayTypeCombo.getSelectionIndex()) { + /* Replace the EventDisplay object with a different subclass */ + setModified(); + String name = eventDisplay.getName(); + EventDisplay newEventDisplay = EventDisplay.eventDisplayFactory(mDisplayTypeCombo.getSelectionIndex(), name); + setCurrentEventDisplay(newEventDisplay); + fillUiWith(newEventDisplay); + } + } + }); + + mChartOptions = new Group(mInfoGroup, SWT.NONE); + mChartOptions.setText("Chart Options"); + GridData gd; + mChartOptions.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + mChartOptions.setLayout(new GridLayout(2, false)); + + Label l = new Label(mChartOptions, SWT.NONE); + l.setText("Time Limit (seconds):"); + + mTimeLimitText = new Text(mChartOptions, SWT.BORDER); + mTimeLimitText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTimeLimitText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent arg0) { + String text = mTimeLimitText.getText().trim(); + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + try { + if (text.length() == 0) { + eventDisplay.resetChartTimeLimit(); + } else { + eventDisplay.setChartTimeLimit(Long.parseLong(text)); + } + } catch (NumberFormatException nfe) { + eventDisplay.resetChartTimeLimit(); + } finally { + setModified(); + } + } + } + }); + + mHistOptions = new Group(mInfoGroup, SWT.NONE); + mHistOptions.setText("Histogram Options"); + GridData gdh; + mHistOptions.setLayoutData(gdh = new GridData(GridData.FILL_HORIZONTAL)); + gdh.horizontalSpan = 2; + mHistOptions.setLayout(new GridLayout(2, false)); + + Label lh = new Label(mHistOptions, SWT.NONE); + lh.setText("Histogram width (hours):"); + + mHistWidthText = new Text(mHistOptions, SWT.BORDER); + mHistWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mHistWidthText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent arg0) { + String text = mHistWidthText.getText().trim(); + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + try { + if (text.length() == 0) { + eventDisplay.resetHistWidth(); + } else { + eventDisplay.setHistWidth(Long.parseLong(text)); + } + } catch (NumberFormatException nfe) { + eventDisplay.resetHistWidth(); + } finally { + setModified(); + } + } + } + }); + + mPidFilterCheckBox = new Button(mInfoGroup, SWT.CHECK); + mPidFilterCheckBox.setText("Enable filtering by pid"); + mPidFilterCheckBox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + mPidFilterCheckBox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + eventDisplay.setPidFiltering(mPidFilterCheckBox.getSelection()); + mPidText.setEnabled(mPidFilterCheckBox.getSelection()); + setModified(); + } + } + }); + + Label pidLabel = new Label(mInfoGroup, SWT.NONE); + pidLabel.setText("Pid Filter:"); + pidLabel.setToolTipText("Enter all pids, separated by commas"); + + mPidText = new Text(mInfoGroup, SWT.BORDER); + mPidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mPidText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mProcessTextChanges) { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null && eventDisplay.getPidFiltering()) { + String pidText = mPidText.getText().trim(); + String[] pids = pidText.split("\\s*,\\s*"); //$NON-NLS-1$ + + ArrayList list = new ArrayList(); + for (String pid : pids) { + try { + list.add(Integer.valueOf(pid)); + } catch (NumberFormatException nfe) { + // just ignore non valid pid + } + } + + eventDisplay.setPidFilterList(list); + setModified(); + } + } + } + }); + + /* ------------------ + * EVENT VALUE/OCCURRENCE SELECTION + * ------------------ */ + mValueSelection = createEventSelection(rightPanel, ValueDisplayDescriptor.class, + "Event Value Display"); + mOccurrenceSelection = createEventSelection(rightPanel, OccurrenceDisplayDescriptor.class, + "Event Occurrence Display"); + } + + private SelectionWidgets createEventSelection(Composite rightPanel, + final Class descriptorClass, + String groupMessage) { + + Group eventSelectionPanel = new Group(rightPanel, SWT.NONE); + eventSelectionPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl; + eventSelectionPanel.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + eventSelectionPanel.setText(groupMessage); + + final SelectionWidgets widgets = new SelectionWidgets(); + + widgets.mList = new List(eventSelectionPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL); + widgets.mList.setLayoutData(new GridData(GridData.FILL_BOTH)); + widgets.mList.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + widgets.mDeleteButton.setEnabled(true); + widgets.mEditButton.setEnabled(true); + } else { + widgets.mDeleteButton.setEnabled(false); + widgets.mEditButton.setEnabled(false); + } + } + }); + + Composite rightControls = new Composite(eventSelectionPanel, SWT.NONE); + rightControls.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + rightControls.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + gl.verticalSpacing = 0; + gl.horizontalSpacing = 0; + + widgets.mNewButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mNewButton.setText("New..."); + widgets.mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mNewButton.setEnabled(false); + widgets.mNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + try { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + EventValueSelector dialog = new EventValueSelector(mShell); + if (dialog.open(descriptorClass, mLogParser)) { + eventDisplay.addDescriptor(dialog.getDescriptor()); + fillUiWith(eventDisplay); + setModified(); + } + } + } catch (Exception e1) { + e1.printStackTrace(); + } + } + }); + + widgets.mEditButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mEditButton.setText("Edit..."); + widgets.mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mEditButton.setEnabled(false); + widgets.mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // get the current descriptor index + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + // get the descriptor itself + OccurrenceDisplayDescriptor descriptor = eventDisplay.getDescriptor( + descriptorClass, index); + + // open the edit dialog. + EventValueSelector dialog = new EventValueSelector(mShell); + if (dialog.open(descriptor, mLogParser)) { + descriptor.replaceWith(dialog.getDescriptor()); + eventDisplay.updateValueDescriptorCheck(); + fillUiWith(eventDisplay); + + // reselect the item since fillUiWith remove the selection. + widgets.mList.select(index); + widgets.mList.notifyListeners(SWT.Selection, null); + + setModified(); + } + } + } + } + }); + + widgets.mDeleteButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); + widgets.mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + widgets.mDeleteButton.setText("Delete"); + widgets.mDeleteButton.setEnabled(false); + widgets.mDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // current event + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // get the current descriptor index + int index = widgets.mList.getSelectionIndex(); + if (index != -1) { + eventDisplay.removeDescriptor(descriptorClass, index); + fillUiWith(eventDisplay); + setModified(); + } + } + } + }); + + return widgets; + } + + + private void duplicateEventDisplay(ArrayList displayList) { + for (EventDisplay eventDisplay : displayList) { + mDisplayList.add(EventDisplay.clone(eventDisplay)); + } + } + + private void buildPidList(ArrayList eventList) { + mPidList = new ArrayList(); + for (EventContainer event : eventList) { + if (mPidList.indexOf(event.pid) == -1) { + mPidList.add(event.pid); + } + } + } + + private void setModified() { + mEditStatus = true; + } + + + private void enable(boolean status) { + mEventDisplayDeleteButton.setEnabled(status); + + // enable up/down + int selection = mEventDisplayList.getSelectionIndex(); + int count = mEventDisplayList.getItemCount(); + mEventDisplayUpButton.setEnabled(status && selection > 0); + mEventDisplayDownButton.setEnabled(status && selection != -1 && selection < count - 1); + + mDisplayNameText.setEnabled(status); + mDisplayTypeCombo.setEnabled(status); + mPidFilterCheckBox.setEnabled(status); + + mValueSelection.setEnabled(status); + mOccurrenceSelection.setEnabled(status); + mValueSelection.mNewButton.setEnabled(status); + mOccurrenceSelection.mNewButton.setEnabled(status); + if (status == false) { + mPidText.setEnabled(false); + } + } + + private void fillEventDisplayList() { + for (EventDisplay eventDisplay : mDisplayList) { + mEventDisplayList.add(eventDisplay.getName()); + } + } + + private void createNewEventDisplay() { + int count = mDisplayList.size(); + + String name = String.format("display %1$d", count + 1); + + EventDisplay eventDisplay = EventDisplay.eventDisplayFactory(0 /* type*/, name); + + mDisplayList.add(eventDisplay); + mEventDisplayList.add(name); + + mEventDisplayList.select(count); + handleEventDisplaySelection(); + mEventDisplayList.showSelection(); + + setModified(); + } + + private void deleteEventDisplay() { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + mDisplayList.remove(selection); + mEventDisplayList.remove(selection); + if (mDisplayList.size() < selection) { + selection--; + } + mEventDisplayList.select(selection); + handleEventDisplaySelection(); + + setModified(); + } + } + + private EventDisplay getCurrentEventDisplay() { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + return mDisplayList.get(selection); + } + + return null; + } + + private void setCurrentEventDisplay(EventDisplay eventDisplay) { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + mDisplayList.set(selection, eventDisplay); + } + } + + private void handleEventDisplaySelection() { + EventDisplay eventDisplay = getCurrentEventDisplay(); + if (eventDisplay != null) { + // enable the UI + enable(true); + + // and fill it + fillUiWith(eventDisplay); + } else { + // disable the UI + enable(false); + + // and empty it. + emptyUi(); + } + } + + private void emptyUi() { + mDisplayNameText.setText(""); + mDisplayTypeCombo.clearSelection(); + mValueSelection.mList.removeAll(); + mOccurrenceSelection.mList.removeAll(); + } + + private void fillUiWith(EventDisplay eventDisplay) { + mProcessTextChanges = false; + + mDisplayNameText.setText(eventDisplay.getName()); + int displayMode = eventDisplay.getDisplayType(); + mDisplayTypeCombo.select(displayMode); + if (displayMode == EventDisplay.DISPLAY_TYPE_GRAPH) { + GridData gd = (GridData) mChartOptions.getLayoutData(); + gd.exclude = false; + mChartOptions.setVisible(!gd.exclude); + long limit = eventDisplay.getChartTimeLimit(); + if (limit != -1) { + mTimeLimitText.setText(Long.toString(limit)); + } else { + mTimeLimitText.setText(""); //$NON-NLS-1$ + } + } else { + GridData gd = (GridData) mChartOptions.getLayoutData(); + gd.exclude = true; + mChartOptions.setVisible(!gd.exclude); + mTimeLimitText.setText(""); //$NON-NLS-1$ + } + + if (displayMode == EventDisplay.DISPLAY_TYPE_SYNC_HIST) { + GridData gd = (GridData) mHistOptions.getLayoutData(); + gd.exclude = false; + mHistOptions.setVisible(!gd.exclude); + long limit = eventDisplay.getHistWidth(); + if (limit != -1) { + mHistWidthText.setText(Long.toString(limit)); + } else { + mHistWidthText.setText(""); //$NON-NLS-1$ + } + } else { + GridData gd = (GridData) mHistOptions.getLayoutData(); + gd.exclude = true; + mHistOptions.setVisible(!gd.exclude); + mHistWidthText.setText(""); //$NON-NLS-1$ + } + mInfoGroup.layout(true); + mShell.layout(true); + mShell.pack(); + + if (eventDisplay.getPidFiltering()) { + mPidFilterCheckBox.setSelection(true); + mPidText.setEnabled(true); + + // build the pid list. + ArrayList list = eventDisplay.getPidFilterList(); + if (list != null) { + StringBuilder sb = new StringBuilder(); + int count = list.size(); + for (int i = 0 ; i < count ; i++) { + sb.append(list.get(i)); + if (i < count - 1) { + sb.append(", ");//$NON-NLS-1$ + } + } + mPidText.setText(sb.toString()); + } else { + mPidText.setText(""); //$NON-NLS-1$ + } + } else { + mPidFilterCheckBox.setSelection(false); + mPidText.setEnabled(false); + mPidText.setText(""); //$NON-NLS-1$ + } + + mProcessTextChanges = true; + + mValueSelection.mList.removeAll(); + mOccurrenceSelection.mList.removeAll(); + + if (eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_FILTERED_LOG || + eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_GRAPH) { + mOccurrenceSelection.setEnabled(true); + mValueSelection.setEnabled(true); + + Iterator valueIterator = eventDisplay.getValueDescriptors(); + + while (valueIterator.hasNext()) { + ValueDisplayDescriptor descriptor = valueIterator.next(); + mValueSelection.mList.add(String.format("%1$s: %2$s [%3$s]%4$s", + mEventTagMap.get(descriptor.eventTag), descriptor.valueName, + getSeriesLabelDescription(descriptor), getFilterDescription(descriptor))); + } + + Iterator occurrenceIterator = + eventDisplay.getOccurrenceDescriptors(); + + while (occurrenceIterator.hasNext()) { + OccurrenceDisplayDescriptor descriptor = occurrenceIterator.next(); + + mOccurrenceSelection.mList.add(String.format("%1$s [%2$s]%3$s", + mEventTagMap.get(descriptor.eventTag), + getSeriesLabelDescription(descriptor), + getFilterDescription(descriptor))); + } + + mValueSelection.mList.notifyListeners(SWT.Selection, null); + mOccurrenceSelection.mList.notifyListeners(SWT.Selection, null); + } else { + mOccurrenceSelection.setEnabled(false); + mValueSelection.setEnabled(false); + } + + } + + /** + * Returns a String describing what is used as the series label + * @param descriptor the descriptor of the display. + */ + private String getSeriesLabelDescription(OccurrenceDisplayDescriptor descriptor) { + if (descriptor.seriesValueIndex != -1) { + if (descriptor.includePid) { + return String.format("%1$s + pid", + mEventDescriptionMap.get( + descriptor.eventTag)[descriptor.seriesValueIndex].getName()); + } else { + return mEventDescriptionMap.get(descriptor.eventTag)[descriptor.seriesValueIndex] + .getName(); + } + } + return "pid"; + } + + private String getFilterDescription(OccurrenceDisplayDescriptor descriptor) { + if (descriptor.filterValueIndex != -1) { + return String.format(" [%1$s %2$s %3$s]", + mEventDescriptionMap.get( + descriptor.eventTag)[descriptor.filterValueIndex].getName(), + descriptor.filterCompareMethod.testString(), + descriptor.filterValue != null ? + descriptor.filterValue.toString() : "?"); //$NON-NLS-1$ + } + return ""; //$NON-NLS-1$ + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java new file mode 100644 index 000000000..a1303f675 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java @@ -0,0 +1,82 @@ +/* + * 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.Log; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +/** + * Imports a textual event log. Gets tags from build path. + */ +public class EventLogImporter { + + private String[] mTags; + private String[] mLog; + + public EventLogImporter(String filePath) throws FileNotFoundException { + String top = System.getenv("ANDROID_BUILD_TOP"); + if (top == null) { + throw new FileNotFoundException(); + } + final String tagFile = top + "/system/core/logcat/event-log-tags"; + BufferedReader tagReader = new BufferedReader( + new InputStreamReader(new FileInputStream(tagFile))); + BufferedReader eventReader = new BufferedReader( + new InputStreamReader(new FileInputStream(filePath))); + try { + readTags(tagReader); + readLog(eventReader); + } catch (IOException e) { + } + } + + public String[] getTags() { + return mTags; + } + + public String[] getLog() { + return mLog; + } + + private void readTags(BufferedReader reader) throws IOException { + String line; + + ArrayList content = new ArrayList(); + while ((line = reader.readLine()) != null) { + content.add(line); + } + mTags = content.toArray(new String[content.size()]); + } + + private void readLog(BufferedReader reader) throws IOException { + String line; + + ArrayList content = new ArrayList(); + while ((line = reader.readLine()) != null) { + content.add(line); + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java new file mode 100644 index 000000000..11138d101 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogPanel.java @@ -0,0 +1,926 @@ +/* + * 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.IDevice; +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 IDevice mCurrentLoggedDevice; + private String mCurrentLogFile; + private LogReceiver mCurrentLogReceiver; + private EventLogParser mCurrentEventLogParser; + + private Object mLock = new Object(); + + /** list of all the events. */ + private final ArrayList mEvents = new ArrayList(); + + /** list of all the new events, that have yet to be displayed by the ui */ + private final ArrayList mNewEvents = new ArrayList(); + /** indicates a pending ui thread display */ + private boolean mPendingDisplay = false; + + /** list of all the custom event displays */ + private final ArrayList mEventDisplays = new ArrayList(); + + 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. + *

This method sets up the {@link ICommonAction} objects to execute the proper code + * when triggered by using {@link ICommonAction#setRunnable(Runnable)}. + *

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. + *

+ * This must be called from the UI thread + */ + @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. + *

+ * This must be called from the UI thread + */ + 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 IDevice 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}. + *

+ * 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); + } + } + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java new file mode 100644 index 000000000..dd32e2c32 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventValueSelector.java @@ -0,0 +1,628 @@ +/* + * 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.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.EventContainer.CompareMethod; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; +import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; + +final class EventValueSelector extends Dialog { + private static final int DLG_WIDTH = 400; + private static final int DLG_HEIGHT = 300; + + private Shell mParent; + private Shell mShell; + private boolean mEditStatus; + private Combo mEventCombo; + private Combo mValueCombo; + private Combo mSeriesCombo; + private Button mDisplayPidCheckBox; + private Combo mFilterCombo; + private Combo mFilterMethodCombo; + private Text mFilterValue; + private Button mOkButton; + + private EventLogParser mLogParser; + private OccurrenceDisplayDescriptor mDescriptor; + + /** list of event integer in the order of the combo. */ + private Integer[] mEventTags; + + /** list of indices in the {@link EventValueDescription} array of the current event + * that are of type string. This lets us get back the {@link EventValueDescription} from the + * index in the Series {@link Combo}. + */ + private final ArrayList mSeriesIndices = new ArrayList(); + + public EventValueSelector(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + } + + /** + * Opens the display option dialog to edit a new descriptor. + * @param decriptorClass the class of the object to instantiate. Must extend + * {@link OccurrenceDisplayDescriptor} + * @param logParser + * @return true if the object is to be created, false if the creation was canceled. + */ + boolean open(Class descriptorClass, + EventLogParser logParser) { + try { + OccurrenceDisplayDescriptor descriptor = descriptorClass.newInstance(); + setModified(); + return open(descriptor, logParser); + } catch (InstantiationException e) { + return false; + } catch (IllegalAccessException e) { + return false; + } + } + + /** + * Opens the display option dialog, to edit a {@link OccurrenceDisplayDescriptor} object or + * a {@link ValueDisplayDescriptor} object. + * @param descriptor The descriptor to edit. + * @return true if the object was modified. + */ + boolean open(OccurrenceDisplayDescriptor descriptor, EventLogParser logParser) { + // make a copy of the descriptor as we'll use a working copy. + if (descriptor instanceof ValueDisplayDescriptor) { + mDescriptor = new ValueDisplayDescriptor((ValueDisplayDescriptor)descriptor); + } else if (descriptor instanceof OccurrenceDisplayDescriptor) { + mDescriptor = new OccurrenceDisplayDescriptor(descriptor); + } else { + return false; + } + + mLogParser = logParser; + + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + loadValueDescriptor(); + + checkValidity(); + + // Set the dialog size. + try { + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + } catch (Exception e) { + e.printStackTrace(); + } + + mShell.layout(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mEditStatus; + } + + OccurrenceDisplayDescriptor getDescriptor() { + return mDescriptor; + } + + private void createUI() { + GridData gd; + + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Event Display Configuration"); + + mShell.setLayout(new GridLayout(2, false)); + + Label l = new Label(mShell, SWT.NONE); + l.setText("Event:"); + + mEventCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mEventCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // the event tag / event name map + Map eventTagMap = mLogParser.getTagMap(); + Map eventInfoMap = mLogParser.getEventInfoMap(); + Set keys = eventTagMap.keySet(); + ArrayList list = new ArrayList(); + for (Integer i : keys) { + if (eventInfoMap.get(i) != null) { + String eventName = eventTagMap.get(i); + mEventCombo.add(eventName); + + list.add(i); + } + } + mEventTags = list.toArray(new Integer[list.size()]); + + mEventCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleEventComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Value:"); + + mValueCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mValueCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mValueCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleValueComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Series Name:"); + + mSeriesCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mSeriesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSeriesCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleSeriesComboSelection(); + setModified(); + } + }); + + // empty comp + new Composite(mShell, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = gd.widthHint = 0; + + mDisplayPidCheckBox = new Button(mShell, SWT.CHECK); + mDisplayPidCheckBox.setText("Also Show pid"); + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mDescriptor.includePid = mDisplayPidCheckBox.getSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter By:"); + + mFilterCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mFilterCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleFilterComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter Method:"); + + mFilterMethodCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mFilterMethodCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (CompareMethod method : CompareMethod.values()) { + mFilterMethodCombo.add(method.toString()); + } + mFilterMethodCombo.select(0); + mFilterMethodCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleFilterMethodComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter Value:"); + + mFilterValue = new Text(mShell, SWT.BORDER | SWT.SINGLE); + mFilterValue.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterValue.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (mDescriptor.filterValueIndex != -1) { + // get the current selection in the event combo + int index = mEventCombo.getSelectionIndex(); + + if (index != -1) { + // match it to an event + int eventTag = mEventTags[index]; + mDescriptor.eventTag = eventTag; + + // get the EventValueDescription for this tag + EventValueDescription valueDesc = mLogParser.getEventInfoMap() + .get(eventTag)[mDescriptor.filterValueIndex]; + + // let the EventValueDescription convert the String value into an object + // of the proper type. + mDescriptor.filterValue = valueDesc.getObjectFromString( + mFilterValue.getText().trim()); + setModified(); + } + } + } + }); + + // add a separator spanning the 2 columns + + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + l.setLayoutData(gd); + + // add a composite to hold the ok/cancel button, no matter what the columns size are. + Composite buttonComp = new Composite(mShell, SWT.NONE); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + buttonComp.setLayoutData(gd); + GridLayout gl; + buttonComp.setLayout(gl = new GridLayout(6, true)); + gl.marginHeight = gl.marginWidth = 0; + + Composite padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mOkButton = new Button(buttonComp, SWT.PUSH); + mOkButton.setText("OK"); + mOkButton.setLayoutData(new GridData(GridData.CENTER)); + mOkButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Button cancelButton = new Button(buttonComp, SWT.PUSH); + cancelButton.setText("Cancel"); + cancelButton.setLayoutData(new GridData(GridData.CENTER)); + cancelButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + // cancel the edit + mEditStatus = false; + mShell.close(); + } + }); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + event.doit = true; + } + }); + } + + private void setModified() { + mEditStatus = true; + } + + private void handleEventComboSelection() { + // get the current selection in the event combo + int index = mEventCombo.getSelectionIndex(); + + if (index != -1) { + // match it to an event + int eventTag = mEventTags[index]; + mDescriptor.eventTag = eventTag; + + // get the EventValueDescription for this tag + EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); + + // fill the combo for the values + mValueCombo.removeAll(); + if (values != null) { + if (mDescriptor instanceof ValueDisplayDescriptor) { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; + + mValueCombo.setEnabled(true); + for (EventValueDescription value : values) { + mValueCombo.add(value.toString()); + } + + if (valueDescriptor.valueIndex != -1) { + mValueCombo.select(valueDescriptor.valueIndex); + } else { + mValueCombo.clearSelection(); + } + } else { + mValueCombo.setEnabled(false); + } + + // fill the axis combo + mSeriesCombo.removeAll(); + mSeriesCombo.setEnabled(false); + mSeriesIndices.clear(); + int axisIndex = 0; + int selectionIndex = -1; + for (EventValueDescription value : values) { + if (value.getEventValueType() == EventValueType.STRING) { + mSeriesCombo.add(value.getName()); + mSeriesCombo.setEnabled(true); + mSeriesIndices.add(axisIndex); + + if (mDescriptor.seriesValueIndex != -1 && + mDescriptor.seriesValueIndex == axisIndex) { + selectionIndex = axisIndex; + } + } + axisIndex++; + } + + if (mSeriesCombo.isEnabled()) { + mSeriesCombo.add("default (pid)", 0 /* index */); + mSeriesIndices.add(0 /* index */, -1 /* value */); + + // +1 because we added another item at index 0 + mSeriesCombo.select(selectionIndex + 1); + + if (selectionIndex >= 0) { + mDisplayPidCheckBox.setSelection(mDescriptor.includePid); + mDisplayPidCheckBox.setEnabled(true); + } else { + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.setSelection(false); + } + } else { + mDisplayPidCheckBox.setSelection(false); + mDisplayPidCheckBox.setEnabled(false); + } + + // fill the filter combo + mFilterCombo.setEnabled(true); + mFilterCombo.removeAll(); + mFilterCombo.add("(no filter)"); + for (EventValueDescription value : values) { + mFilterCombo.add(value.toString()); + } + + // select the current filter + mFilterCombo.select(mDescriptor.filterValueIndex + 1); + mFilterMethodCombo.select(getFilterMethodIndex(mDescriptor.filterCompareMethod)); + + // fill the current filter value + if (mDescriptor.filterValueIndex != -1) { + EventValueDescription valueInfo = values[mDescriptor.filterValueIndex]; + if (valueInfo.checkForType(mDescriptor.filterValue)) { + mFilterValue.setText(mDescriptor.filterValue.toString()); + } else { + mFilterValue.setText(""); + } + } else { + mFilterValue.setText(""); + } + } else { + disableSubCombos(); + } + } else { + disableSubCombos(); + } + + checkValidity(); + } + + /** + * + */ + private void disableSubCombos() { + mValueCombo.removeAll(); + mValueCombo.clearSelection(); + mValueCombo.setEnabled(false); + + mSeriesCombo.removeAll(); + mSeriesCombo.clearSelection(); + mSeriesCombo.setEnabled(false); + + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.setSelection(false); + + mFilterCombo.removeAll(); + mFilterCombo.clearSelection(); + mFilterCombo.setEnabled(false); + + mFilterValue.setEnabled(false); + mFilterValue.setText(""); + mFilterMethodCombo.setEnabled(false); + } + + private void handleValueComboSelection() { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; + + // get the current selection in the value combo + int index = mValueCombo.getSelectionIndex(); + valueDescriptor.valueIndex = index; + + // for now set the built-in name + + // get the current selection in the event combo + int eventIndex = mEventCombo.getSelectionIndex(); + + // match it to an event + int eventTag = mEventTags[eventIndex]; + + // get the EventValueDescription for this tag + EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); + + valueDescriptor.valueName = values[index].getName(); + + checkValidity(); + } + + private void handleSeriesComboSelection() { + // get the current selection in the axis combo + int index = mSeriesCombo.getSelectionIndex(); + + // get the actual value index from the list. + int valueIndex = mSeriesIndices.get(index); + + mDescriptor.seriesValueIndex = valueIndex; + + if (index > 0) { + mDisplayPidCheckBox.setEnabled(true); + mDisplayPidCheckBox.setSelection(mDescriptor.includePid); + } else { + mDisplayPidCheckBox.setSelection(false); + mDisplayPidCheckBox.setEnabled(false); + } + } + + private void handleFilterComboSelection() { + // get the current selection in the axis combo + int index = mFilterCombo.getSelectionIndex(); + + // decrement index by 1 since the item 0 means + // no filter (index = -1), and the rest is offset by 1 + index--; + + mDescriptor.filterValueIndex = index; + + if (index != -1) { + mFilterValue.setEnabled(true); + mFilterMethodCombo.setEnabled(true); + if (mDescriptor.filterValue instanceof String) { + mFilterValue.setText((String)mDescriptor.filterValue); + } + } else { + mFilterValue.setText(""); + mFilterValue.setEnabled(false); + mFilterMethodCombo.setEnabled(false); + } + } + + private void handleFilterMethodComboSelection() { + // get the current selection in the axis combo + int index = mFilterMethodCombo.getSelectionIndex(); + CompareMethod method = CompareMethod.values()[index]; + + mDescriptor.filterCompareMethod = method; + } + + /** + * Returns the index of the filter method + * @param filterCompareMethod the {@link CompareMethod} enum. + */ + private int getFilterMethodIndex(CompareMethod filterCompareMethod) { + CompareMethod[] values = CompareMethod.values(); + for (int i = 0 ; i < values.length ; i++) { + if (values[i] == filterCompareMethod) { + return i; + } + } + return -1; + } + + + private void loadValueDescriptor() { + // get the index from the eventTag. + int eventIndex = 0; + int comboIndex = -1; + for (int i : mEventTags) { + if (i == mDescriptor.eventTag) { + comboIndex = eventIndex; + break; + } + eventIndex++; + } + + if (comboIndex == -1) { + mEventCombo.clearSelection(); + } else { + mEventCombo.select(comboIndex); + } + + // get the event from the descriptor + handleEventComboSelection(); + } + + private void checkValidity() { + mOkButton.setEnabled(mEventCombo.getSelectionIndex() != -1 && + (((mDescriptor instanceof ValueDisplayDescriptor) == false) || + mValueCombo.getSelectionIndex() != -1)); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java new file mode 100644 index 000000000..3af14470b --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/OccurrenceRenderer.java @@ -0,0 +1,90 @@ +/* + * 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 org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.CrosshairState; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYItemRendererState; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.data.xy.XYDataset; +import org.jfree.ui.RectangleEdge; + +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; + +/** + * Custom renderer to render event occurrence. This rendered ignores the y value, and simply + * draws a line from min to max at the time of the item. + */ +public class OccurrenceRenderer extends XYLineAndShapeRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public void drawItem(Graphics2D g2, + XYItemRendererState state, + Rectangle2D dataArea, + PlotRenderingInfo info, + XYPlot plot, + ValueAxis domainAxis, + ValueAxis rangeAxis, + XYDataset dataset, + int series, + int item, + CrosshairState crosshairState, + int pass) { + TimeSeriesCollection timeDataSet = (TimeSeriesCollection)dataset; + + // get the x value for the series/item. + double x = timeDataSet.getX(series, item).doubleValue(); + + // get the min/max of the range axis + double yMin = rangeAxis.getLowerBound(); + double yMax = rangeAxis.getUpperBound(); + + RectangleEdge domainEdge = plot.getDomainAxisEdge(); + RectangleEdge rangeEdge = plot.getRangeAxisEdge(); + + // convert the coordinates to java2d. + double x2D = domainAxis.valueToJava2D(x, dataArea, domainEdge); + double yMin2D = rangeAxis.valueToJava2D(yMin, dataArea, rangeEdge); + double yMax2D = rangeAxis.valueToJava2D(yMax, dataArea, rangeEdge); + + // get the paint information for the series/item + Paint p = getItemPaint(series, item); + Stroke s = getItemStroke(series, item); + + Line2D line = null; + PlotOrientation orientation = plot.getOrientation(); + if (orientation == PlotOrientation.HORIZONTAL) { + line = new Line2D.Double(yMin2D, x2D, yMax2D, x2D); + } + else if (orientation == PlotOrientation.VERTICAL) { + line = new Line2D.Double(x2D, yMin2D, x2D, yMax2D); + } + g2.setPaint(p); + g2.setStroke(s); + g2.draw(line); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java new file mode 100644 index 000000000..0a355d1e9 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2009 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.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; + +import java.awt.Color; + +abstract public class SyncCommon extends EventDisplay { + + // State information while processing the event stream + private int mLastState; // 0 if event started, 1 if event stopped + private long mLastStartTime; // ms + private long mLastStopTime; //ms + private String mLastDetails; + private int mLastSyncSource; // poll, server, user, etc. + + // Some common variables for sync display. These define the sync backends + //and how they should be displayed. + protected static final int CALENDAR = 0; + protected static final int GMAIL = 1; + protected static final int FEEDS = 2; + protected static final int CONTACTS = 3; + protected static final int ERRORS = 4; + protected static final int NUM_AUTHS = (CONTACTS + 1); + protected static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", + "Errors"}; + protected static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, + Color.ORANGE, Color.RED}; + + // Values from data/etc/event-log-tags + final int EVENT_SYNC = 2720; + final int EVENT_TICKLE = 2742; + final int EVENT_SYNC_DETAILS = 2743; + final int EVENT_CONTACTS_AGGREGATION = 2747; + + protected SyncCommon(String name) { + super(name); + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + mLastStartTime = 0; + mLastStopTime = 0; + mLastState = -1; + mLastSyncSource = -1; + mLastDetails = ""; + } + + /** + * Updates the display with a new event. This is the main entry point for + * each event. This method has the logic to tie together the start event, + * stop event, and details event into one graph item. The combined sync event + * is handed to the subclass via processSycnEvent. Note that the details + * can happen before or after the stop event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + try { + if (event.mTag == EVENT_SYNC) { + int state = Integer.parseInt(event.getValueAsString(1)); + if (state == 0) { // start + mLastStartTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + mLastState = 0; + mLastSyncSource = Integer.parseInt(event.getValueAsString(2)); + mLastDetails = ""; + } else if (state == 1) { // stop + if (mLastState == 0) { + mLastStopTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + if (mLastStartTime == 0) { + // Log starts with a stop event + mLastStartTime = mLastStopTime; + } + int auth = getAuth(event.getValueAsString(0)); + processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, + true, mLastSyncSource); + mLastState = 1; + } + } + } else if (event.mTag == EVENT_SYNC_DETAILS) { + mLastDetails = event.getValueAsString(3); + if (mLastState != 0) { // Not inside event + long updateTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + if (updateTime - mLastStopTime <= 250) { + // Got details within 250ms after event, so delete and re-insert + // Details later than 250ms (arbitrary) are discarded as probably + // unrelated. + int auth = getAuth(event.getValueAsString(0)); + processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, + false, mLastSyncSource); + } + } + } else if (event.mTag == EVENT_CONTACTS_AGGREGATION) { + long stopTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + long startTime = stopTime - Long.parseLong(event.getValueAsString(0)); + String details; + int count = Integer.parseInt(event.getValueAsString(1)); + if (count < 0) { + details = "g" + (-count); + } else { + details = "G" + count; + } + processSyncEvent(event, CONTACTS, startTime, stopTime, details, + true /* newEvent */, mLastSyncSource); + } + } catch (InvalidTypeException e) { + } + } + + /** + * Callback hook for subclass to process a sync event. newEvent has the logic + * to combine start and stop events and passes a processed event to the + * subclass. + * + * @param event The sync event + * @param auth The sync authority + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource Poll, user, server, etc. + */ + abstract void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource); + + /** + * Converts authority name to auth number. + * + * @param authname "calendar", etc. + * @return number series number associated with the authority + */ + protected int getAuth(String authname) throws InvalidTypeException { + if ("calendar".equals(authname) || "cl".equals(authname)) { + return CALENDAR; + } else if ("contacts".equals(authname) || "cp".equals(authname) || + "com.android.contacts".equals(authname)) { + return CONTACTS; + } else if ("subscribedfeeds".equals(authname)) { + return FEEDS; + } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) { + return GMAIL; + } else if ("gmail-live".equals(authname)) { + return GMAIL; + } else if ("unknown".equals(authname)) { + return -1; // Unknown tickles; discard + } else { + throw new InvalidTypeException("Unknown authname " + authname); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java new file mode 100644 index 000000000..c66fe4844 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/EditFilterDialog.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2007 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.logcat; + +import com.android.ddmuilib.IImageLoader; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Small dialog box to edit a static port number. + */ +public class EditFilterDialog extends Dialog { + + private static final int DLG_WIDTH = 400; + private static final int DLG_HEIGHT = 250; + + private Shell mParent; + + private Shell mShell; + + private boolean mOk = false; + + private IImageLoader mImageLoader; + + /** + * Filter being edited or created + */ + private LogFilter mFilter; + + private String mName; + private String mTag; + private String mPid; + + /** Log level as an index of the drop-down combo + * @see getLogLevel + * @see getComboIndex + */ + private int mLogLevel; + + private Button mOkButton; + + private Label mPidWarning; + + public EditFilterDialog(IImageLoader imageLoader, Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageLoader = imageLoader; + } + + public EditFilterDialog(IImageLoader imageLoader, Shell shell, + LogFilter filter) { + this(imageLoader, shell); + mFilter = filter; + } + + /** + * Opens the dialog. The method will return when the user closes the dialog + * somehow. + * + * @return true if ok was pressed, false if cancelled. + */ + public boolean open() { + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + + mShell.open(); + + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + // we're quitting with OK. + // Lets update the filter if needed + if (mOk) { + // if it was a "Create filter" action we need to create it first. + if (mFilter == null) { + mFilter = new LogFilter(mName); + } + + // setup the filter + mFilter.setTagMode(mTag); + + if (mPid != null && mPid.length() > 0) { + mFilter.setPidMode(Integer.parseInt(mPid)); + } else { + mFilter.setPidMode(-1); + } + + mFilter.setLogLevel(getLogLevel(mLogLevel)); + } + + return mOk; + } + + public LogFilter getFilter() { + return mFilter; + } + + private void createUI() { + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Log Filter"); + + mShell.setLayout(new GridLayout(1, false)); + + mShell.addListener(SWT.Close, new Listener() { + public void handleEvent(Event event) { + } + }); + + // top part with the filter name + Composite nameComposite = new Composite(mShell, SWT.NONE); + nameComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + nameComposite.setLayout(new GridLayout(2, false)); + + Label l = new Label(nameComposite, SWT.NONE); + l.setText("Filter Name:"); + + final Text filterNameText = new Text(nameComposite, + SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + mName = mFilter.getName(); + if (mName != null) { + filterNameText.setText(mName); + } + } + filterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + filterNameText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mName = filterNameText.getText().trim(); + validate(); + } + }); + + // separator + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + + // center part with the filter parameters + Composite main = new Composite(mShell, SWT.NONE); + main.setLayoutData(new GridData(GridData.FILL_BOTH)); + main.setLayout(new GridLayout(3, false)); + + l = new Label(main, SWT.NONE); + l.setText("by Log Tag:"); + + final Text tagText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + mTag = mFilter.getTagFilter(); + if (mTag != null) { + tagText.setText(mTag); + } + } + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + tagText.setLayoutData(gd); + tagText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mTag = tagText.getText().trim(); + validate(); + } + }); + + l = new Label(main, SWT.NONE); + l.setText("by pid:"); + + final Text pidText = new Text(main, SWT.SINGLE | SWT.BORDER); + if (mFilter != null) { + if (mFilter.getPidFilter() != -1) { + mPid = Integer.toString(mFilter.getPidFilter()); + } else { + mPid = ""; + } + pidText.setText(mPid); + } + pidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + pidText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mPid = pidText.getText().trim(); + validate(); + } + }); + + mPidWarning = new Label(main, SWT.NONE); + mPidWarning.setImage(mImageLoader.loadImage("empty.png", // $NON-NLS-1$ + mShell.getDisplay())); + + l = new Label(main, SWT.NONE); + l.setText("by Log level:"); + + final Combo logCombo = new Combo(main, SWT.DROP_DOWN | SWT.READ_ONLY); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + logCombo.setLayoutData(gd); + + // add the labels + logCombo.add(""); + logCombo.add("Error"); + logCombo.add("Warning"); + logCombo.add("Info"); + logCombo.add("Debug"); + logCombo.add("Verbose"); + + if (mFilter != null) { + mLogLevel = getComboIndex(mFilter.getLogLevel()); + logCombo.select(mLogLevel); + } else { + logCombo.select(0); + } + + logCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection + mLogLevel = logCombo.getSelectionIndex(); + validate(); + } + }); + + // separator + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // bottom part with the ok/cancel + Composite bottomComp = new Composite(mShell, SWT.NONE); + bottomComp + .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + bottomComp.setLayout(new GridLayout(2, true)); + + mOkButton = new Button(bottomComp, SWT.NONE); + mOkButton.setText("OK"); + mOkButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mOk = true; + mShell.close(); + } + }); + mOkButton.setEnabled(false); + mShell.setDefaultButton(mOkButton); + + Button cancelButton = new Button(bottomComp, SWT.NONE); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + validate(); + } + + /** + * Returns the log level from a combo index. + * @param index the Combo index + * @return a log level valid for the Log class. + */ + protected int getLogLevel(int index) { + if (index == 0) { + return -1; + } + + return 7 - index; + } + + /** + * Returns the index in the combo that matches the log level + * @param logLevel The Log level. + * @return the combo index + */ + private int getComboIndex(int logLevel) { + if (logLevel == -1) { + return 0; + } + + return 7 - logLevel; + } + + /** + * Validates the content of the 2 text fields and enable/disable "ok", while + * setting up the warning/error message. + */ + private void validate() { + + // then we check it only contains digits. + if (mPid != null) { + if (mPid.matches("[0-9]*") == false) { // $NON-NLS-1$ + mOkButton.setEnabled(false); + mPidWarning.setImage(mImageLoader.loadImage( + "warning.png", // $NON-NLS-1$ + mShell.getDisplay())); + return; + } else { + mPidWarning.setImage(mImageLoader.loadImage( + "empty.png", // $NON-NLS-1$ + mShell.getDisplay())); + } + } + + if (mName == null || mName.length() == 0) { + mOkButton.setEnabled(false); + return; + } + + mOkButton.setEnabled(true); + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java new file mode 100644 index 000000000..9cff6568d --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogColors.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2007 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.logcat; + +import org.eclipse.swt.graphics.Color; + +public class LogColors { + public Color infoColor; + public Color debugColor; + public Color errorColor; + public Color warningColor; + public Color verboseColor; +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java new file mode 100644 index 000000000..a32de2fd5 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogFilter.java @@ -0,0 +1,555 @@ +/* + * Copyright (C) 2007 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.logcat; + +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.annotation.UiThread; +import com.android.ddmuilib.logcat.LogPanel.LogMessage; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.regex.PatternSyntaxException; + +/** logcat output filter class */ +public class LogFilter { + + public final static int MODE_PID = 0x01; + public final static int MODE_TAG = 0x02; + public final static int MODE_LEVEL = 0x04; + + private String mName; + + /** + * Filtering mode. Value can be a mix of MODE_PID, MODE_TAG, MODE_LEVEL + */ + private int mMode = 0; + + /** + * pid used for filtering. Only valid if mMode is MODE_PID. + */ + private int mPid; + + /** Single level log level as defined in Log.mLevelChar. Only valid + * if mMode is MODE_LEVEL */ + private int mLogLevel; + + /** + * log tag filtering. Only valid if mMode is MODE_TAG + */ + private String mTag; + + private Table mTable; + private TabItem mTabItem; + private boolean mIsCurrentTabItem = false; + private int mUnreadCount = 0; + + /** Temp keyword filtering */ + private String[] mTempKeywordFilters; + + /** temp pid filtering */ + private int mTempPid = -1; + + /** temp tag filtering */ + private String mTempTag; + + /** temp log level filtering */ + private int mTempLogLevel = -1; + + private LogColors mColors; + + private boolean mTempFilteringStatus = false; + + private final ArrayList mMessages = new ArrayList(); + private final ArrayList mNewMessages = new ArrayList(); + + private boolean mSupportsDelete = true; + private boolean mSupportsEdit = true; + private int mRemovedMessageCount = 0; + + /** + * Creates a filter with a particular mode. + * @param name The name to be displayed in the UI + */ + public LogFilter(String name) { + mName = name; + } + + public LogFilter() { + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(mName); + + sb.append(':'); + sb.append(mMode); + if ((mMode & MODE_PID) == MODE_PID) { + sb.append(':'); + sb.append(mPid); + } + + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + sb.append(':'); + sb.append(mLogLevel); + } + + if ((mMode & MODE_TAG) == MODE_TAG) { + sb.append(':'); + sb.append(mTag); + } + + return sb.toString(); + } + + public boolean loadFromString(String string) { + String[] segments = string.split(":"); // $NON-NLS-1$ + int index = 0; + + // get the name + mName = segments[index++]; + + // get the mode + mMode = Integer.parseInt(segments[index++]); + + if ((mMode & MODE_PID) == MODE_PID) { + mPid = Integer.parseInt(segments[index++]); + } + + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + mLogLevel = Integer.parseInt(segments[index++]); + } + + if ((mMode & MODE_TAG) == MODE_TAG) { + mTag = segments[index++]; + } + + return true; + } + + + /** Sets the name of the filter. */ + void setName(String name) { + mName = name; + } + + /** + * Returns the UI display name. + */ + public String getName() { + return mName; + } + + /** + * Set the Table ui widget associated with this filter. + * @param tabItem The item in the TabFolder + * @param table The Table object + */ + public void setWidgets(TabItem tabItem, Table table) { + mTable = table; + mTabItem = tabItem; + } + + /** + * Returns true if the filter is ready for ui. + */ + public boolean uiReady() { + return (mTable != null && mTabItem != null); + } + + /** + * Returns the UI table object. + * @return + */ + public Table getTable() { + return mTable; + } + + public void dispose() { + mTable.dispose(); + mTabItem.dispose(); + mTable = null; + mTabItem = null; + } + + /** + * Resets the filtering mode to be 0 (i.e. no filter). + */ + public void resetFilteringMode() { + mMode = 0; + } + + /** + * Returns the current filtering mode. + * @return A bitmask. Possible values are MODE_PID, MODE_TAG, MODE_LEVEL + */ + public int getFilteringMode() { + return mMode; + } + + /** + * Adds PID to the current filtering mode. + * @param pid + */ + public void setPidMode(int pid) { + if (pid != -1) { + mMode |= MODE_PID; + } else { + mMode &= ~MODE_PID; + } + mPid = pid; + } + + /** Returns the pid filter if valid, otherwise -1 */ + public int getPidFilter() { + if ((mMode & MODE_PID) == MODE_PID) + return mPid; + return -1; + } + + public void setTagMode(String tag) { + if (tag != null && tag.length() > 0) { + mMode |= MODE_TAG; + } else { + mMode &= ~MODE_TAG; + } + mTag = tag; + } + + public String getTagFilter() { + if ((mMode & MODE_TAG) == MODE_TAG) + return mTag; + return null; + } + + public void setLogLevel(int level) { + if (level == -1) { + mMode &= ~MODE_LEVEL; + } else { + mMode |= MODE_LEVEL; + mLogLevel = level; + } + + } + + public int getLogLevel() { + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + return mLogLevel; + } + + return -1; + } + + + public boolean supportsDelete() { + return mSupportsDelete ; + } + + public boolean supportsEdit() { + return mSupportsEdit; + } + + /** + * Sets the selected state of the filter. + * @param selected selection state. + */ + public void setSelectedState(boolean selected) { + if (selected) { + if (mTabItem != null) { + mTabItem.setText(mName); + } + mUnreadCount = 0; + } + mIsCurrentTabItem = selected; + } + + /** + * Adds a new message and optionally removes an old message. + *

The new message is filtered through {@link #accept(LogMessage)}. + * Calls to {@link #flush()} from a UI thread will display it (and other + * pending messages) to the associated {@link Table}. + * @param logMessage the MessageData object to filter + * @return true if the message was accepted. + */ + public boolean addMessage(LogMessage newMessage, LogMessage oldMessage) { + synchronized (mMessages) { + if (oldMessage != null) { + int index = mMessages.indexOf(oldMessage); + if (index != -1) { + // TODO check that index will always be -1 or 0, as only the oldest message is ever removed. + mMessages.remove(index); + mRemovedMessageCount++; + } + + // now we look for it in mNewMessages. This can happen if the new message is added + // and then removed because too many messages are added between calls to #flush() + index = mNewMessages.indexOf(oldMessage); + if (index != -1) { + // TODO check that index will always be -1 or 0, as only the oldest message is ever removed. + mNewMessages.remove(index); + } + } + + boolean filter = accept(newMessage); + + if (filter) { + // at this point the message is accepted, we add it to the list + mMessages.add(newMessage); + mNewMessages.add(newMessage); + } + + return filter; + } + } + + /** + * Removes all the items in the filter and its {@link Table}. + */ + public void clear() { + mRemovedMessageCount = 0; + mNewMessages.clear(); + mMessages.clear(); + mTable.removeAll(); + } + + /** + * Filters a message. + * @param logMessage the Message + * @return true if the message is accepted by the filter. + */ + boolean accept(LogMessage logMessage) { + // do the regular filtering now + if ((mMode & MODE_PID) == MODE_PID && mPid != logMessage.data.pid) { + return false; + } + + if ((mMode & MODE_TAG) == MODE_TAG && ( + logMessage.data.tag == null || + logMessage.data.tag.equals(mTag) == false)) { + return false; + } + + int msgLogLevel = logMessage.data.logLevel.getPriority(); + + // test the temp log filtering first, as it replaces the old one + if (mTempLogLevel != -1) { + if (mTempLogLevel > msgLogLevel) { + return false; + } + } else if ((mMode & MODE_LEVEL) == MODE_LEVEL && + mLogLevel > msgLogLevel) { + return false; + } + + // do the temp filtering now. + if (mTempKeywordFilters != null) { + String msg = logMessage.msg; + + for (String kw : mTempKeywordFilters) { + try { + if (msg.contains(kw) == false && msg.matches(kw) == false) { + return false; + } + } catch (PatternSyntaxException e) { + // if the string is not a valid regular expression, + // this exception is thrown. + return false; + } + } + } + + if (mTempPid != -1 && mTempPid != logMessage.data.pid) { + return false; + } + + if (mTempTag != null && mTempTag.length() > 0) { + if (mTempTag.equals(logMessage.data.tag) == false) { + return false; + } + } + + return true; + } + + /** + * Takes all the accepted messages and display them. + * This must be called from a UI thread. + */ + @UiThread + public void flush() { + // if scroll bar is at the bottom, we will scroll + ScrollBar bar = mTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // if we are not going to scroll, get the current first item being shown. + int topIndex = mTable.getTopIndex(); + + // disable drawing + mTable.setRedraw(false); + + int totalCount = mNewMessages.size(); + + try { + // remove the items of the old messages. + for (int i = 0 ; i < mRemovedMessageCount && mTable.getItemCount() > 0 ; i++) { + mTable.remove(0); + } + + if (mUnreadCount > mTable.getItemCount()) { + mUnreadCount = mTable.getItemCount(); + } + + // add the new items + for (int i = 0 ; i < totalCount ; i++) { + LogMessage msg = mNewMessages.get(i); + addTableItem(msg); + } + } catch (SWTException e) { + // log the error and keep going. Content of the logcat table maybe unexpected + // but at least ddms won't crash. + Log.e("LogFilter", e); + } + + // redraw + mTable.setRedraw(true); + + // scroll if needed, by showing the last item + if (scroll) { + totalCount = mTable.getItemCount(); + if (totalCount > 0) { + mTable.showItem(mTable.getItem(totalCount-1)); + } + } else if (mRemovedMessageCount > 0) { + // we need to make sure the topIndex is still visible. + // Because really old items are removed from the list, this could make it disappear + // if we don't change the scroll value at all. + + topIndex -= mRemovedMessageCount; + if (topIndex < 0) { + // looks like it disappeared. Lets just show the first item + mTable.showItem(mTable.getItem(0)); + } else { + mTable.showItem(mTable.getItem(topIndex)); + } + } + + // if this filter is not the current one, we update the tab text + // with the amount of unread message + if (mIsCurrentTabItem == false) { + mUnreadCount += mNewMessages.size(); + totalCount = mTable.getItemCount(); + if (mUnreadCount > 0) { + mTabItem.setText(mName + " (" // $NON-NLS-1$ + + (mUnreadCount > totalCount ? totalCount : mUnreadCount) + + ")"); // $NON-NLS-1$ + } else { + mTabItem.setText(mName); // $NON-NLS-1$ + } + } + + mNewMessages.clear(); + } + + void setColors(LogColors colors) { + mColors = colors; + } + + int getUnreadCount() { + return mUnreadCount; + } + + void setUnreadCount(int unreadCount) { + mUnreadCount = unreadCount; + } + + void setSupportsDelete(boolean support) { + mSupportsDelete = support; + } + + void setSupportsEdit(boolean support) { + mSupportsEdit = support; + } + + void setTempKeywordFiltering(String[] segments) { + mTempKeywordFilters = segments; + mTempFilteringStatus = true; + } + + void setTempPidFiltering(int pid) { + mTempPid = pid; + mTempFilteringStatus = true; + } + + void setTempTagFiltering(String tag) { + mTempTag = tag; + mTempFilteringStatus = true; + } + + void resetTempFiltering() { + if (mTempPid != -1 || mTempTag != null || mTempKeywordFilters != null) { + mTempFilteringStatus = true; + } + + mTempPid = -1; + mTempTag = null; + mTempKeywordFilters = null; + } + + void resetTempFilteringStatus() { + mTempFilteringStatus = false; + } + + boolean getTempFilterStatus() { + return mTempFilteringStatus; + } + + + /** + * Add a TableItem for the index-th item of the buffer + * @param filter The index of the table in which to insert the item. + */ + private void addTableItem(LogMessage msg) { + TableItem item = new TableItem(mTable, SWT.NONE); + item.setText(0, msg.data.time); + item.setText(1, new String(new char[] { msg.data.logLevel.getPriorityLetter() })); + item.setText(2, msg.data.pidString); + item.setText(3, msg.data.tag); + item.setText(4, msg.msg); + + // add the buffer index as data + item.setData(msg); + + if (msg.data.logLevel == LogLevel.INFO) { + item.setForeground(mColors.infoColor); + } else if (msg.data.logLevel == LogLevel.DEBUG) { + item.setForeground(mColors.debugColor); + } else if (msg.data.logLevel == LogLevel.ERROR) { + item.setForeground(mColors.errorColor); + } else if (msg.data.logLevel == LogLevel.WARN) { + item.setForeground(mColors.warningColor); + } else { + item.setForeground(mColors.verboseColor); + } + } +} diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java new file mode 100644 index 000000000..82c125903 --- /dev/null +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogPanel.java @@ -0,0 +1,1584 @@ +/* + * Copyright (C) 2007 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.logcat; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.SelectionDependentPanel; +import com.android.ddmuilib.TableHelper; +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; +import com.android.ddmuilib.actions.ICommonAction; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +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.Label; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LogPanel extends SelectionDependentPanel { + + private static final int STRING_BUFFER_LENGTH = 10000; + + /** no filtering. Only one tab with everything. */ + public static final int FILTER_NONE = 0; + /** manual mode for filter. all filters are manually created. */ + public static final int FILTER_MANUAL = 1; + /** automatic mode for filter (pid mode). + * All filters are automatically created. */ + public static final int FILTER_AUTO_PID = 2; + /** automatic mode for filter (tag mode). + * All filters are automatically created. */ + public static final int FILTER_AUTO_TAG = 3; + /** Manual filtering mode + new filter for debug app, if needed */ + public static final int FILTER_DEBUG = 4; + + public static final int COLUMN_MODE_MANUAL = 0; + public static final int COLUMN_MODE_AUTO = 1; + + public static String PREFS_TIME; + public static String PREFS_LEVEL; + public static String PREFS_PID; + public static String PREFS_TAG; + public static String PREFS_MESSAGE; + + /** + * This pattern is meant to parse the first line of a log message with the option + * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the + * following lines are the message (can be several line).
+ * This first line looks something like
+ * "[ 00-00 00:00:00.000 <pid>:0x<???> <severity>/<tag>]" + *
+ * Note: severity is one of V, D, I, W, or EM
+ * Note: the fraction of second value can have any number of digit. + * Note the tag should be trim as it may have spaces at the end. + */ + private static Pattern sLogPattern = Pattern.compile( + "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + //$NON-NLS-1$ + "\\s+(\\d*):(0x[0-9a-fA-F]+)\\s([VDIWE])/(.*)\\]$"); //$NON-NLS-1$ + + /** + * Interface for Storage Filter manager. Implementation of this interface + * provide a custom way to archive an reload filters. + */ + public interface ILogFilterStorageManager { + + public LogFilter[] getFilterFromStore(); + + public void saveFilters(LogFilter[] filters); + + public boolean requiresDefaultFilter(); + } + + private Composite mParent; + private IPreferenceStore mStore; + + /** top object in the view */ + private TabFolder mFolders; + + private LogColors mColors; + + private ILogFilterStorageManager mFilterStorage; + + private LogCatOuputReceiver mCurrentLogCat; + + /** + * Circular buffer containing the logcat output. This is unfiltered. + * The valid content goes from mBufferStart to + * mBufferEnd - 1. Therefore its number of item is + * mBufferEnd - mBufferStart. + */ + private LogMessage[] mBuffer = new LogMessage[STRING_BUFFER_LENGTH]; + + /** Represents the oldest message in the buffer */ + private int mBufferStart = -1; + + /** + * Represents the next usable item in the buffer to receive new message. + * This can be equal to mBufferStart, but when used mBufferStart will be + * incremented as well. + */ + private int mBufferEnd = -1; + + /** Filter list */ + private LogFilter[] mFilters; + + /** Default filter */ + private LogFilter mDefaultFilter; + + /** Current filter being displayed */ + private LogFilter mCurrentFilter; + + /** Filtering mode */ + private int mFilterMode = FILTER_NONE; + + /** Device currently running logcat */ + private IDevice mCurrentLoggedDevice = null; + + private ICommonAction mDeleteFilterAction; + private ICommonAction mEditFilterAction; + + private ICommonAction[] mLogLevelActions; + + /** message data, separated from content for multi line messages */ + protected static class LogMessageInfo { + public LogLevel logLevel; + public int pid; + public String pidString; + public String tag; + public String time; + } + + /** pointer to the latest LogMessageInfo. this is used for multi line + * log message, to reuse the info regarding level, pid, etc... + */ + private LogMessageInfo mLastMessageInfo = null; + + private boolean mPendingAsyncRefresh = false; + + /** loader for the images. the implementation will varie between standalone + * app and eclipse plugin app and eclipse plugin. */ + private IImageLoader mImageLoader; + + private String mDefaultLogSave; + + private int mColumnMode = COLUMN_MODE_MANUAL; + private Font mDisplayFont; + + private ITableFocusListener mGlobalListener; + + /** message data, separated from content for multi line messages */ + protected static class LogMessage { + public LogMessageInfo data; + public String msg; + + @Override + public String toString() { + return data.time + ": " //$NON-NLS-1$ + + data.logLevel + "/" //$NON-NLS-1$ + + data.tag + "(" //$NON-NLS-1$ + + data.pidString + "): " //$NON-NLS-1$ + + msg; + } + } + + /** + * objects able to receive the output of a remote shell command, + * specifically a logcat command in this case + */ + private final class LogCatOuputReceiver extends MultiLineReceiver { + + public boolean isCancelled = false; + + public LogCatOuputReceiver() { + super(); + + setTrimLine(false); + } + + @Override + public void processNewLines(String[] lines) { + if (isCancelled == false) { + processLogLines(lines); + } + } + + public boolean isCancelled() { + return isCancelled; + } + } + + /** + * Parser class for the output of a "ps" shell command executed on a device. + * This class looks for a specific pid to find the process name from it. + * Once found, the name is used to update a filter and a tab object + * + */ + private class PsOutputReceiver extends MultiLineReceiver { + + private LogFilter mFilter; + + private TabItem mTabItem; + + private int mPid; + + /** set to true when we've found the pid we're looking for */ + private boolean mDone = false; + + PsOutputReceiver(int pid, LogFilter filter, TabItem tabItem) { + mPid = pid; + mFilter = filter; + mTabItem = tabItem; + } + + public boolean isCancelled() { + return mDone; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.startsWith("USER")) { //$NON-NLS-1$ + continue; + } + // get the pid. + int index = line.indexOf(' '); + if (index == -1) { + continue; + } + // look for the next non blank char + index++; + while (line.charAt(index) == ' ') { + index++; + } + + // this is the start of the pid. + // look for the end. + int index2 = line.indexOf(' ', index); + + // get the line + String pidStr = line.substring(index, index2); + int pid = Integer.parseInt(pidStr); + if (pid != mPid) { + continue; + } else { + // get the process name + index = line.lastIndexOf(' '); + final String name = line.substring(index + 1); + + mFilter.setName(name); + + // update the tab + Display d = mFolders.getDisplay(); + d.asyncExec(new Runnable() { + public void run() { + mTabItem.setText(name); + } + }); + + // we're done with this ps. + mDone = true; + return; + } + } + } + + } + + + /** + * Create the log view with some default parameters + * @param imageLoader the image loader. + * @param colors The display color object + * @param filterStorage the storage for user defined filters. + * @param mode The filtering mode + */ + public LogPanel(IImageLoader imageLoader, LogColors colors, + ILogFilterStorageManager filterStorage, int mode) { + mImageLoader = imageLoader; + mColors = colors; + mFilterMode = mode; + mFilterStorage = filterStorage; + mStore = DdmUiPreferences.getStore(); + } + + public void setActions(ICommonAction deleteAction, ICommonAction editAction, + ICommonAction[] logLevelActions) { + mDeleteFilterAction = deleteAction; + mEditFilterAction = editAction; + mLogLevelActions = logLevelActions; + } + + /** + * Sets the column mode. Must be called before creatUI + * @param mode the column mode. Valid values are COLUMN_MOD_MANUAL and + * COLUMN_MODE_AUTO + */ + public void setColumnMode(int mode) { + mColumnMode = mode; + } + + /** + * Sets the display font. + * @param font The display font. + */ + public void setFont(Font font) { + mDisplayFont = font; + + if (mFilters != null) { + for (LogFilter f : mFilters) { + Table table = f.getTable(); + if (table != null) { + table.setFont(font); + } + } + } + + if (mDefaultFilter != null) { + Table table = mDefaultFilter.getTable(); + if (table != null) { + table.setFont(font); + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + startLogCat(getCurrentDevice()); + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + // pass + } + + + /** + * Creates a control capable of displaying some information. This is + * called once, when the application is initializing, from the UI thread. + */ + @Override + protected Control createControl(Composite parent) { + mParent = parent; + + Composite top = new Composite(parent, SWT.NONE); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + top.setLayout(new GridLayout(1, false)); + + // create the tab folder + mFolders = new TabFolder(top, SWT.NONE); + mFolders.setLayoutData(new GridData(GridData.FILL_BOTH)); + mFolders.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentFilter != null) { + mCurrentFilter.setSelectedState(false); + } + mCurrentFilter = getCurrentFilter(); + mCurrentFilter.setSelectedState(true); + updateColumns(mCurrentFilter.getTable()); + if (mCurrentFilter.getTempFilterStatus()) { + initFilter(mCurrentFilter); + } + selectionChanged(mCurrentFilter); + } + }); + + + Composite bottom = new Composite(top, SWT.NONE); + bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + bottom.setLayout(new GridLayout(3, false)); + + Label label = new Label(bottom, SWT.NONE); + label.setText("Filter:"); + + final Text filterText = new Text(bottom, SWT.SINGLE | SWT.BORDER); + filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + filterText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + updateFilteringWith(filterText.getText()); + } + }); + + /* + Button addFilterBtn = new Button(bottom, SWT.NONE); + addFilterBtn.setImage(mImageLoader.loadImage("add.png", //$NON-NLS-1$ + addFilterBtn.getDisplay())); + */ + + // get the filters + createFilters(); + + // for each filter, create a tab. + int index = 0; + + if (mDefaultFilter != null) { + createTab(mDefaultFilter, index++, false); + } + + if (mFilters != null) { + for (LogFilter f : mFilters) { + createTab(f, index++, false); + } + } + + return top; + } + + @Override + protected void postCreation() { + // pass + } + + /** + * Sets the focus to the proper object. + */ + @Override + public void setFocus() { + mFolders.setFocus(); + } + + + /** + * Starts a new logcat and set mCurrentLogCat as the current receiver. + * @param device the device to connect logcat to. + */ + public void startLogCat(final IDevice device) { + if (device == mCurrentLoggedDevice) { + return; + } + + // if we have a logcat already running + if (mCurrentLoggedDevice != null) { + stopLogCat(false); + mCurrentLoggedDevice = null; + } + + resetUI(false); + + if (device != null) { + // create a new output receiver + mCurrentLogCat = new LogCatOuputReceiver(); + + // start the logcat in a different thread + new Thread("Logcat") { //$NON-NLS-1$ + @Override + public void run() { + + while (device.isOnline() == false && + mCurrentLogCat != null && + mCurrentLogCat.isCancelled == false) { + try { + sleep(2000); + } catch (InterruptedException e) { + return; + } + } + + if (mCurrentLogCat == null || mCurrentLogCat.isCancelled) { + // logcat was stopped/cancelled before the device became ready. + return; + } + + try { + mCurrentLoggedDevice = device; + device.executeShellCommand("logcat -v long", mCurrentLogCat); //$NON-NLS-1$ + } catch (Exception e) { + Log.e("Logcat", e); + } finally { + // at this point the command is terminated. + mCurrentLogCat = null; + mCurrentLoggedDevice = null; + } + } + }.start(); + } + } + + /** Stop the current logcat */ + public void stopLogCat(boolean inUiThread) { + if (mCurrentLogCat != null) { + mCurrentLogCat.isCancelled = true; + + // when the thread finishes, no one will reference that object + // and it'll be destroyed + mCurrentLogCat = null; + + // reset the content buffer + for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) { + mBuffer[i] = null; + } + + // because it's a circular buffer, it's hard to know if + // the array is empty with both start/end at 0 or if it's full + // with both start/end at 0 as well. So to mean empty, we use -1 + mBufferStart = -1; + mBufferEnd = -1; + + resetFilters(); + resetUI(inUiThread); + } + } + + /** + * Adds a new Filter. This methods displays the UI to create the filter + * and set up its parameters.
+ * MUST be called from the ui thread. + * + */ + public void addFilter() { + EditFilterDialog dlg = new EditFilterDialog(mImageLoader, + mFolders.getShell()); + if (dlg.open()) { + synchronized (mBuffer) { + // get the new filter in the array + LogFilter filter = dlg.getFilter(); + addFilterToArray(filter); + + int index = mFilters.length - 1; + if (mDefaultFilter != null) { + index++; + } + + if (false) { + + for (LogFilter f : mFilters) { + if (f.uiReady()) { + f.dispose(); + } + } + if (mDefaultFilter != null && mDefaultFilter.uiReady()) { + mDefaultFilter.dispose(); + } + + // for each filter, create a tab. + int i = 0; + if (mFilters != null) { + for (LogFilter f : mFilters) { + createTab(f, i++, true); + } + } + if (mDefaultFilter != null) { + createTab(mDefaultFilter, i++, true); + } + } else { + + // create ui for the filter. + createTab(filter, index, true); + + // reset the default as it shouldn't contain the content of + // this new filter. + if (mDefaultFilter != null) { + initDefaultFilter(); + } + } + + // select the new filter + if (mCurrentFilter != null) { + mCurrentFilter.setSelectedState(false); + } + mFolders.setSelection(index); + filter.setSelectedState(true); + mCurrentFilter = filter; + + selectionChanged(filter); + + // finally we update the filtering mode if needed + if (mFilterMode == FILTER_NONE) { + mFilterMode = FILTER_MANUAL; + } + + mFilterStorage.saveFilters(mFilters); + + } + } + } + + /** + * Edits the current filter. The method displays the UI to edit the filter. + */ + public void editFilter() { + if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) { + EditFilterDialog dlg = new EditFilterDialog(mImageLoader, + mFolders.getShell(), + mCurrentFilter); + if (dlg.open()) { + synchronized (mBuffer) { + // at this point the filter has been updated. + // so we update its content + initFilter(mCurrentFilter); + + // and the content of the "other" filter as well. + if (mDefaultFilter != null) { + initDefaultFilter(); + } + + mFilterStorage.saveFilters(mFilters); + } + } + } + } + + /** + * Deletes the current filter. + */ + public void deleteFilter() { + synchronized (mBuffer) { + if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) { + // remove the filter from the list + removeFilterFromArray(mCurrentFilter); + mCurrentFilter.dispose(); + + // select the new filter + mFolders.setSelection(0); + if (mFilters.length > 0) { + mCurrentFilter = mFilters[0]; + } else { + mCurrentFilter = mDefaultFilter; + } + + selectionChanged(mCurrentFilter); + + // update the content of the "other" filter to include what was filtered out + // by the deleted filter. + if (mDefaultFilter != null) { + initDefaultFilter(); + } + + mFilterStorage.saveFilters(mFilters); + } + } + } + + /** + * saves the current selection in a text file. + * @return false if the saving failed. + */ + public boolean save() { + synchronized (mBuffer) { + FileDialog dlg = new FileDialog(mParent.getShell(), SWT.SAVE); + String fileName; + + dlg.setText("Save log..."); + dlg.setFileName("log.txt"); + String defaultPath = mDefaultLogSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + dlg.setFilterPath(defaultPath); + dlg.setFilterNames(new String[] { + "Text Files (*.txt)" + }); + dlg.setFilterExtensions(new String[] { + "*.txt" + }); + + fileName = dlg.open(); + if (fileName != null) { + mDefaultLogSave = dlg.getFilterPath(); + + // get the current table and its selection + Table currentTable = mCurrentFilter.getTable(); + + int[] selection = currentTable.getSelectionIndices(); + + // we need to sort the items to be sure. + Arrays.sort(selection); + + // loop on the selection and output the file. + try { + FileWriter writer = new FileWriter(fileName); + + for (int i : selection) { + TableItem item = currentTable.getItem(i); + LogMessage msg = (LogMessage)item.getData(); + String line = msg.toString(); + writer.write(line); + writer.write('\n'); + } + writer.flush(); + + } catch (IOException e) { + return false; + } + } + } + + return true; + } + + /** + * Empty the current circular buffer. + */ + public void clear() { + synchronized (mBuffer) { + for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) { + mBuffer[i] = null; + } + + mBufferStart = -1; + mBufferEnd = -1; + + // now we clear the existing filters + for (LogFilter filter : mFilters) { + filter.clear(); + } + + // and the default one + if (mDefaultFilter != null) { + mDefaultFilter.clear(); + } + } + } + + /** + * Copies the current selection of the current filter as multiline text. + * + * @param clipboard The clipboard to place the copied content. + */ + public void copy(Clipboard clipboard) { + // get the current table and its selection + Table currentTable = mCurrentFilter.getTable(); + + copyTable(clipboard, currentTable); + } + + /** + * Selects all lines. + */ + public void selectAll() { + Table currentTable = mCurrentFilter.getTable(); + currentTable.selectAll(); + } + + /** + * Sets a TableFocusListener which will be notified when one of the tables + * gets or loses focus. + * + * @param listener + */ + public void setTableFocusListener(ITableFocusListener listener) { + // record the global listener, to make sure table created after + // this call will still be setup. + mGlobalListener = listener; + + // now we setup the existing filters + for (LogFilter filter : mFilters) { + Table table = filter.getTable(); + + addTableToFocusListener(table); + } + + // and the default one + if (mDefaultFilter != null) { + addTableToFocusListener(mDefaultFilter.getTable()); + } + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus. + * + * @param table the Table object. + */ + private void addTableToFocusListener(final Table table) { + // create the activator for this table + final IFocusedTableActivator activator = new IFocusedTableActivator() { + public void copy(Clipboard clipboard) { + copyTable(clipboard, table); + } + + public void selectAll() { + table.selectAll(); + } + }; + + // add the focus listener on the table to notify the global listener + table.addFocusListener(new FocusListener() { + public void focusGained(FocusEvent e) { + mGlobalListener.focusGained(activator); + } + + public void focusLost(FocusEvent e) { + mGlobalListener.focusLost(activator); + } + }); + } + + /** + * Copies the current selection of a Table into the provided Clipboard, as + * multi-line text. + * + * @param clipboard The clipboard to place the copied content. + * @param table The table to copy from. + */ + private static void copyTable(Clipboard clipboard, Table table) { + int[] selection = table.getSelectionIndices(); + + // we need to sort the items to be sure. + Arrays.sort(selection); + + // all lines must be concatenated. + StringBuilder sb = new StringBuilder(); + + // loop on the selection and output the file. + for (int i : selection) { + TableItem item = table.getItem(i); + LogMessage msg = (LogMessage)item.getData(); + String line = msg.toString(); + sb.append(line); + sb.append('\n'); + } + + // now add that to the clipboard + clipboard.setContents(new Object[] { + sb.toString() + }, new Transfer[] { + TextTransfer.getInstance() + }); + } + + /** + * Sets the log level for the current filter, but does not save it. + * @param i + */ + public void setCurrentFilterLogLevel(int i) { + LogFilter filter = getCurrentFilter(); + + filter.setLogLevel(i); + + initFilter(filter); + } + + /** + * Creates a new tab in the folderTab item. Must be called from the ui + * thread. + * @param filter The filter associated with the tab. + * @param index the index of the tab. if -1, the tab will be added at the + * end. + * @param fillTable If true the table is filled with the current content of + * the buffer. + * @return The TabItem object that was created. + */ + private TabItem createTab(LogFilter filter, int index, boolean fillTable) { + synchronized (mBuffer) { + TabItem item = null; + if (index != -1) { + item = new TabItem(mFolders, SWT.NONE, index); + } else { + item = new TabItem(mFolders, SWT.NONE); + } + item.setText(filter.getName()); + + // set the control (the parent is the TabFolder item, always) + Composite top = new Composite(mFolders, SWT.NONE); + item.setControl(top); + + top.setLayout(new FillLayout()); + + // create the ui, first the table + final Table t = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + + if (mDisplayFont != null) { + t.setFont(mDisplayFont); + } + + // give the ui objects to the filters. + filter.setWidgets(item, t); + + t.setHeaderVisible(true); + t.setLinesVisible(false); + + if (mGlobalListener != null) { + addTableToFocusListener(t); + } + + // create a controllistener that will handle the resizing of all the + // columns (except the last) and of the table itself. + ControlListener listener = null; + if (mColumnMode == COLUMN_MODE_AUTO) { + listener = new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + Rectangle r = t.getClientArea(); + + // get the size of all but the last column + int total = t.getColumn(0).getWidth(); + total += t.getColumn(1).getWidth(); + total += t.getColumn(2).getWidth(); + total += t.getColumn(3).getWidth(); + + if (r.width > total) { + t.getColumn(4).setWidth(r.width-total); + } + } + }; + + t.addControlListener(listener); + } + + // then its column + TableColumn col = TableHelper.createTableColumn(t, "Time", SWT.LEFT, + "00-00 00:00:00", //$NON-NLS-1$ + PREFS_TIME, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + col.addControlListener(listener); + } + + col = TableHelper.createTableColumn(t, "", SWT.CENTER, + "D", //$NON-NLS-1$ + PREFS_LEVEL, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + col.addControlListener(listener); + } + + col = TableHelper.createTableColumn(t, "pid", SWT.LEFT, + "9999", //$NON-NLS-1$ + PREFS_PID, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + col.addControlListener(listener); + } + + col = TableHelper.createTableColumn(t, "tag", SWT.LEFT, + "abcdefgh", //$NON-NLS-1$ + PREFS_TAG, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + col.addControlListener(listener); + } + + col = TableHelper.createTableColumn(t, "Message", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz0123456789", //$NON-NLS-1$ + PREFS_MESSAGE, mStore); + if (mColumnMode == COLUMN_MODE_AUTO) { + // instead of listening on resize for the last column, we make + // it non resizable. + col.setResizable(false); + } + + if (fillTable) { + initFilter(filter); + } + return item; + } + } + + protected void updateColumns(Table table) { + if (table != null) { + int index = 0; + TableColumn col; + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_TIME)); + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_LEVEL)); + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_PID)); + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_TAG)); + + col = table.getColumn(index++); + col.setWidth(mStore.getInt(PREFS_MESSAGE)); + } + } + + public void resetUI(boolean inUiThread) { + if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) { + if (inUiThread) { + mFolders.dispose(); + mParent.pack(true); + createControl(mParent); + } else { + Display d = mFolders.getDisplay(); + + // run sync as we need to update right now. + d.syncExec(new Runnable() { + public void run() { + mFolders.dispose(); + mParent.pack(true); + createControl(mParent); + } + }); + } + } else { + // the ui is static we just empty it. + if (mFolders.isDisposed() == false) { + if (inUiThread) { + emptyTables(); + } else { + Display d = mFolders.getDisplay(); + + // run sync as we need to update right now. + d.syncExec(new Runnable() { + public void run() { + if (mFolders.isDisposed() == false) { + emptyTables(); + } + } + }); + } + } + } + } + + /** + * Process new Log lines coming from {@link LogCatOuputReceiver}. + * @param lines the new lines + */ + protected void processLogLines(String[] lines) { + // WARNING: this will not work if the string contains more line than + // the buffer holds. + + if (lines.length > STRING_BUFFER_LENGTH) { + Log.e("LogCat", "Receiving more lines than STRING_BUFFER_LENGTH"); + } + + // parse the lines and create LogMessage that are stored in a temporary list + final ArrayList newMessages = new ArrayList(); + + synchronized (mBuffer) { + for (String line : lines) { + // ignore empty lines. + if (line.length() > 0) { + // check for header lines. + Matcher matcher = sLogPattern.matcher(line); + if (matcher.matches()) { + // this is a header line, parse the header and keep it around. + mLastMessageInfo = new LogMessageInfo(); + + mLastMessageInfo.time = matcher.group(1); + mLastMessageInfo.pidString = matcher.group(2); + mLastMessageInfo.pid = Integer.valueOf(mLastMessageInfo.pidString); + mLastMessageInfo.logLevel = LogLevel.getByLetterString(matcher.group(4)); + mLastMessageInfo.tag = matcher.group(5).trim(); + } else { + // This is not a header line. + // Create a new LogMessage and process it. + LogMessage mc = new LogMessage(); + + if (mLastMessageInfo == null) { + // The first line of output wasn't preceded + // by a header line; make something up so + // that users of mc.data don't NPE. + mLastMessageInfo = new LogMessageInfo(); + mLastMessageInfo.time = "??-?? ??:??:??.???"; //$NON-NLS1$ + mLastMessageInfo.pidString = ""; //$NON-NLS1$ + mLastMessageInfo.pid = 0; + mLastMessageInfo.logLevel = LogLevel.INFO; + mLastMessageInfo.tag = ""; //$NON-NLS1$ + } + + // If someone printed a log message with + // embedded '\n' characters, there will + // one header line followed by multiple text lines. + // Use the last header that we saw. + mc.data = mLastMessageInfo; + + // tabs seem to display as only 1 tab so we replace the leading tabs + // by 4 spaces. + mc.msg = line.replaceAll("\t", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + // process the new LogMessage. + processNewMessage(mc); + + // store the new LogMessage + newMessages.add(mc); + } + } + } + + // if we don't have a pending Runnable that will do the refresh, we ask the Display + // to run one in the UI thread. + if (mPendingAsyncRefresh == false) { + mPendingAsyncRefresh = true; + + try { + Display display = mFolders.getDisplay(); + + // run in sync because this will update the buffer start/end indices + display.asyncExec(new Runnable() { + public void run() { + asyncRefresh(); + } + }); + } catch (SWTException e) { + // display is disposed, we're probably quitting. Let's stop. + stopLogCat(false); + } + } + } + } + + /** + * Refreshes the UI with new messages. + */ + private void asyncRefresh() { + if (mFolders.isDisposed() == false) { + synchronized (mBuffer) { + try { + // the circular buffer has been updated, let have the filter flush their + // display with the new messages. + if (mFilters != null) { + for (LogFilter f : mFilters) { + f.flush(); + } + } + + if (mDefaultFilter != null) { + mDefaultFilter.flush(); + } + } finally { + // the pending refresh is done. + mPendingAsyncRefresh = false; + } + } + } else { + stopLogCat(true); + } + } + + /** + * Processes a new Message. + *

This adds the new message to the buffer, and gives it to the existing filters. + * @param newMessage + */ + private void processNewMessage(LogMessage newMessage) { + // if we are in auto filtering mode, make sure we have + // a filter for this + if (mFilterMode == FILTER_AUTO_PID || + mFilterMode == FILTER_AUTO_TAG) { + checkFilter(newMessage.data); + } + + // compute the index where the message goes. + // was the buffer empty? + int messageIndex = -1; + if (mBufferStart == -1) { + messageIndex = mBufferStart = 0; + mBufferEnd = 1; + } else { + messageIndex = mBufferEnd; + + // check we aren't overwriting start + if (mBufferEnd == mBufferStart) { + mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH; + } + + // increment the next usable slot index + mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH; + } + + LogMessage oldMessage = null; + + // record the message that was there before + if (mBuffer[messageIndex] != null) { + oldMessage = mBuffer[messageIndex]; + } + + // then add the new one + mBuffer[messageIndex] = newMessage; + + // give the new message to every filters. + boolean filtered = false; + if (mFilters != null) { + for (LogFilter f : mFilters) { + filtered |= f.addMessage(newMessage, oldMessage); + } + } + if (filtered == false && mDefaultFilter != null) { + mDefaultFilter.addMessage(newMessage, oldMessage); + } + } + + private void createFilters() { + if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) { + // unarchive the filters. + mFilters = mFilterStorage.getFilterFromStore(); + + // set the colors + if (mFilters != null) { + for (LogFilter f : mFilters) { + f.setColors(mColors); + } + } + + if (mFilterStorage.requiresDefaultFilter()) { + mDefaultFilter = new LogFilter("Log"); + mDefaultFilter.setColors(mColors); + mDefaultFilter.setSupportsDelete(false); + mDefaultFilter.setSupportsEdit(false); + } + } else if (mFilterMode == FILTER_NONE) { + // if the filtering mode is "none", we create a single filter that + // will receive all + mDefaultFilter = new LogFilter("Log"); + mDefaultFilter.setColors(mColors); + mDefaultFilter.setSupportsDelete(false); + mDefaultFilter.setSupportsEdit(false); + } + } + + /** Checks if there's an automatic filter for this md and if not + * adds the filter and the ui. + * This must be called from the UI! + * @param md + * @return true if the filter existed already + */ + private boolean checkFilter(final LogMessageInfo md) { + if (true) + return true; + // look for a filter that matches the pid + if (mFilterMode == FILTER_AUTO_PID) { + for (LogFilter f : mFilters) { + if (f.getPidFilter() == md.pid) { + return true; + } + } + } else if (mFilterMode == FILTER_AUTO_TAG) { + for (LogFilter f : mFilters) { + if (f.getTagFilter().equals(md.tag)) { + return true; + } + } + } + + // if we reach this point, no filter was found. + // create a filter with a temporary name of the pid + final LogFilter newFilter = new LogFilter(md.pidString); + String name = null; + if (mFilterMode == FILTER_AUTO_PID) { + newFilter.setPidMode(md.pid); + + // ask the monitor thread if it knows the pid. + name = mCurrentLoggedDevice.getClientName(md.pid); + } else { + newFilter.setTagMode(md.tag); + name = md.tag; + } + addFilterToArray(newFilter); + + final String fname = name; + + // create the tabitem + final TabItem newTabItem = createTab(newFilter, -1, true); + + // if the name is unknown + if (fname == null) { + // we need to find the process running under that pid. + // launch a thread do a ps on the device + new Thread("remote PS") { //$NON-NLS-1$ + @Override + public void run() { + // create the receiver + PsOutputReceiver psor = new PsOutputReceiver(md.pid, + newFilter, newTabItem); + + // execute ps + try { + mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$ + } catch (IOException e) { + // hmm... + } + } + }.start(); + } + + return false; + } + + /** + * Adds a new filter to the current filter array, and set its colors + * @param newFilter The filter to add + */ + private void addFilterToArray(LogFilter newFilter) { + // set the colors + newFilter.setColors(mColors); + + // add it to the array. + if (mFilters != null && mFilters.length > 0) { + LogFilter[] newFilters = new LogFilter[mFilters.length+1]; + System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length); + newFilters[mFilters.length] = newFilter; + mFilters = newFilters; + } else { + mFilters = new LogFilter[1]; + mFilters[0] = newFilter; + } + } + + private void removeFilterFromArray(LogFilter oldFilter) { + // look for the index + int index = -1; + for (int i = 0 ; i < mFilters.length ; i++) { + if (mFilters[i] == oldFilter) { + index = i; + break; + } + } + + if (index != -1) { + LogFilter[] newFilters = new LogFilter[mFilters.length-1]; + System.arraycopy(mFilters, 0, newFilters, 0, index); + System.arraycopy(mFilters, index + 1, newFilters, index, + newFilters.length-index); + mFilters = newFilters; + } + } + + /** + * Initialize the filter with already existing buffer. + * @param filter + */ + private void initFilter(LogFilter filter) { + // is it empty + if (filter.uiReady() == false) { + return; + } + + if (filter == mDefaultFilter) { + initDefaultFilter(); + return; + } + + filter.clear(); + + if (mBufferStart != -1) { + int max = mBufferEnd; + if (mBufferEnd < mBufferStart) { + max += STRING_BUFFER_LENGTH; + } + + for (int i = mBufferStart; i < max; i++) { + int realItemIndex = i % STRING_BUFFER_LENGTH; + + filter.addMessage(mBuffer[realItemIndex], null /* old message */); + } + } + + filter.flush(); + filter.resetTempFilteringStatus(); + } + + /** + * Refill the default filter. Not to be called directly. + * @see initFilter() + */ + private void initDefaultFilter() { + mDefaultFilter.clear(); + + if (mBufferStart != -1) { + int max = mBufferEnd; + if (mBufferEnd < mBufferStart) { + max += STRING_BUFFER_LENGTH; + } + + for (int i = mBufferStart; i < max; i++) { + int realItemIndex = i % STRING_BUFFER_LENGTH; + LogMessage msg = mBuffer[realItemIndex]; + + // first we check that the other filters don't take this message + boolean filtered = false; + for (LogFilter f : mFilters) { + filtered |= f.accept(msg); + } + + if (filtered == false) { + mDefaultFilter.addMessage(msg, null /* old message */); + } + } + } + + mDefaultFilter.flush(); + mDefaultFilter.resetTempFilteringStatus(); + } + + /** + * Reset the filters, to handle change in device in automatic filter mode + */ + private void resetFilters() { + // if we are in automatic mode, then we need to rmove the current + // filter. + if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) { + mFilters = null; + + // recreate the filters. + createFilters(); + } + } + + + private LogFilter getCurrentFilter() { + int index = mFolders.getSelectionIndex(); + + // if mFilters is null or index is invalid, we return the default + // filter. It doesn't matter if that one is null as well, since we + // would return null anyway. + if (index == 0 || mFilters == null) { + return mDefaultFilter; + } + + return mFilters[index-1]; + } + + + private void emptyTables() { + for (LogFilter f : mFilters) { + f.getTable().removeAll(); + } + + if (mDefaultFilter != null) { + mDefaultFilter.getTable().removeAll(); + } + } + + protected void updateFilteringWith(String text) { + synchronized (mBuffer) { + // reset the temp filtering for all the filters + for (LogFilter f : mFilters) { + f.resetTempFiltering(); + } + if (mDefaultFilter != null) { + mDefaultFilter.resetTempFiltering(); + } + + // now we need to figure out the new temp filtering + // split each word + String[] segments = text.split(" "); //$NON-NLS-1$ + + ArrayList keywords = new ArrayList(segments.length); + + // loop and look for temp id/tag + int tempPid = -1; + String tempTag = null; + for (int i = 0 ; i < segments.length; i++) { + String s = segments[i]; + if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$ + // get the pid + String[] seg = s.split(":"); //$NON-NLS-1$ + if (seg.length == 2) { + if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$ + tempPid = Integer.valueOf(seg[1]); + } + } + } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$ + String seg[] = segments[i].split(":"); //$NON-NLS-1$ + if (seg.length == 2) { + tempTag = seg[1]; + } + } else { + keywords.add(s); + } + } + + // set the temp filtering in the filters + if (tempPid != -1 || tempTag != null || keywords.size() > 0) { + String[] keywordsArray = keywords.toArray( + new String[keywords.size()]); + + for (LogFilter f : mFilters) { + if (tempPid != -1) { + f.setTempPidFiltering(tempPid); + } + if (tempTag != null) { + f.setTempTagFiltering(tempTag); + } + f.setTempKeywordFiltering(keywordsArray); + } + + if (mDefaultFilter != null) { + if (tempPid != -1) { + mDefaultFilter.setTempPidFiltering(tempPid); + } + if (tempTag != null) { + mDefaultFilter.setTempTagFiltering(tempTag); + } + mDefaultFilter.setTempKeywordFiltering(keywordsArray); + + } + } + + initFilter(mCurrentFilter); + } + } + + /** + * Called when the current filter selection changes. + * @param selectedFilter + */ + private void selectionChanged(LogFilter selectedFilter) { + if (mLogLevelActions != null) { + // get the log level + int level = selectedFilter.getLogLevel(); + for (int i = 0 ; i < mLogLevelActions.length; i++) { + ICommonAction a = mLogLevelActions[i]; + if (i == level - 2) { + a.setChecked(true); + } else { + a.setChecked(false); + } + } + } + + if (mDeleteFilterAction != null) { + mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete()); + } + if (mEditFilterAction != null) { + mEditFilterAction.setEnabled(selectedFilter.supportsEdit()); + } + } + + public String getSelectedErrorLineMessage() { + Table table = mCurrentFilter.getTable(); + int[] selection = table.getSelectionIndices(); + + if (selection.length == 1) { + TableItem item = table.getItem(selection[0]); + LogMessage msg = (LogMessage)item.getData(); + if (msg.data.logLevel == LogLevel.ERROR || msg.data.logLevel == LogLevel.WARN) + return msg.msg; + } + return null; + } +} diff --git a/ddms/libs/ddmuilib/src/resources/images/add.png b/ddms/libs/ddmuilib/src/resources/images/add.png new file mode 100644 index 0000000000000000000000000000000000000000..eefc2ca300a00e1d80a61c8ddf21416d8e4215db GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k)AG&Ar`&K2@)9xI)b0gxKJ;$ z@2d5hteGFA)*s?ISiw-o+`%9seSj%_;crRV2OPp;B91ZtT*dc)7C*OSk@y84w~dmq tGOHP1eDs-G;=rKLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0009-Nkl%WGU!0D$rD+{et^napG+p~)m>5^1EhShP{G#ZqyhQj5!~EAffC z6H%e7fJN*^5M7B-=tg{?g7_9HBGf8^5mTy#KwD#)$#WjLckVss+;fhL`~%-_^@&U0 zzDTLs!a0M@Gd#ao4NLWXg=pkZP$;(q)>zeFmhtLB*8guw_HHDeaO5&2OL_zV;P8Wp zVrA@bW9sNLg{W~nEY+q34or&8`O=`6o zhCUOMO^ShrfdVxEp|LKb-|G?Y2ryM_F2O3CQP#`iwPmZi zIckeL4>rkEk*~g6geZp^5LrO86Yr43J2;hLjl>zLwH*8#B;6Z}ac`qtvDKNQbE7QW zeG?ykvBEFEuMiLV?4J&J>G|8(?(ERn_RuN?r^sb|&1$veCB5~fPW#Gdalebr5?*+8 zipP$RV~xjJTd}#BAaqKzA*h5IT4gvRiMO{tFO2{QfXDF3ojlx+)((8P^aeeu-dRP0#md(?N>L256O{ zWQtN5!s%VP+xfEFZl6iI{RL|+fe-=*B-^V$sCaYLj!ivLD26A(f`6+Z_i+kmWUjLK zO499o6mM^Tp2hKH1PCE^XA(kSbA>W;vD>~n?+4!5VySr0=5kVC&~k9y<@&PGYFSAs ft+fc@0r39-#Zt?&-GO_;00000NkvXXu0mjf3G~f^ literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/backward.png b/ddms/libs/ddmuilib/src/resources/images/backward.png new file mode 100644 index 0000000000000000000000000000000000000000..90a97137a2f418e0d926ce3b3c9f860b472174d8 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`{+=$5Ar`&K2@(n)+oS&zBn8V9q_lngiX2Azf+24zdO2bWxW$|X8Ciq3a*k!(1xFp1|t inm}Y?+gXKE3=F?d^GL9{&AtINn!(f6&t;ucLK6TNjVDF` literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/clear.png b/ddms/libs/ddmuilib/src/resources/images/clear.png new file mode 100644 index 0000000000000000000000000000000000000000..0009cf662bc7ef4491e64a5b4298dd95df28f82c GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`3p`yMLo9lyPK@SbR^)Ki_bhL^ zJ!5{}-rEgZ@BFiv7FaXo2#-a9joU$ey+7je3>B@t+jl#MUd}7euY4=XAnrLMl1pJ{ z=%Sf|4$-xD-!3#~XgHXVR+6$|qm}WENS=luFX>&MMKdxBivF*3Y{}64z@W^qa%W3i zntB#PRqI{F^FRNHGE6whHDRk`n$pA13}@Ol%THoRI~*APtIFKo;ny#2>uI%~zpma7 Q1iF>M)78&qol`;+02!536aWAK literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/d.png b/ddms/libs/ddmuilib/src/resources/images/d.png new file mode 100644 index 0000000000000000000000000000000000000000..d45506ee79252ad2c51ea6cd15aa8ee5538638e4 GIT binary patch literal 638 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU~=_zaSW-Lll15Re|u)Dgp`yc zA}I+8KY%>tT@pZL|Nkd78UFk8^RSMx@@I)ruyl-`}8o zwsQA6=d>=4rBj|WgLKE7GvsA3;5p*ZULo(oz&dMlXsEls`sa*QYRX4a_I*tb%`4yD zzzy*ctAwIK<|OsQK#%ZDoR}>E0!2kdU>f3H03goGJZrCM=k|Nr}&{^8@yqX&RsLvgwe z^QNUxH9tY&ylqMKg9FTMZEegv?R>TgH}?M*V`gUNZ=O5X21qj>o^M|-vCMDoDTB&S zPh_APLD6ON7HIJ7hbJbg^RV;TI2iV{wstODc8u@b^a&FZDn36uJ73jHyubc`-EUwd8bjr!8H`(I&iwiR$;ruvCh6(v|JzUfZ_E4#v<4Ws zKz}+JKCoAL?%TGLP1b*&jpc7(w4FKe3KFNx42v5|I{K9?W`hj#boFyt=akR{0PtNH AMF0Q* literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-attach.png b/ddms/libs/ddmuilib/src/resources/images/debug-attach.png new file mode 100644 index 0000000000000000000000000000000000000000..9b8a11c40aa561ad975bc58e8f5d79c3868853a7 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`$(}BbAr`$`gWUNT6gZq8|KHyu zUMMQE_}dXL&E^-v6_3M@-1a-T^3WP#R*jk7Q+oFtI@6V!xgsv8>Di%%HaVS-vmD$a zPQGKD?z#7|!tedfcQq!=U%EZ`kv!YdyqlGOjv6nmOVOAi%`0pf5eKx6!PC{xWt~$( F695LOJC6VW literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/debug-error.png b/ddms/libs/ddmuilib/src/resources/images/debug-error.png new file mode 100644 index 0000000000000000000000000000000000000000..f22da1fffa8e6c9ab032bcc03e65c59a25429555 GIT binary patch literal 222 zcmV<403rX0P)t<#W!~WT?oM#%&h)=?Fur695x=omFdS9Rv`=v5rssjmcM7gMUoaAZPq# z5|uZ2-vnS5D@_Ea{4};BG$z}Yj&$k>8FIz@A|Kcj8*ll116s%6>FVdQ&MBb@ E0K+yna{vGU literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/delete.png b/ddms/libs/ddmuilib/src/resources/images/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..db5fab8e41d3845ef8beb199500042c3f8494387 GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`MxHK?Ar`&K2@zopr E0D=D>LI3~& literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/device.png b/ddms/libs/ddmuilib/src/resources/images/device.png new file mode 100644 index 0000000000000000000000000000000000000000..7dbbbb6a4b6d8e6d3d862fc36531df6b469133b9 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`ex5FlAr`&KDF^ueJr+n$0D>bY z6`hRR?8I_5S$~r~wDR)dlYFh`U0QN04z6Gf>2Q0a_oLq+?fNy@7fvMwjAz1Pz8DlU gnlW=oHZ5XjXpvSd|9FS57icnrr>mdKI;Vst0Q2K6DgXcg literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/down.png b/ddms/libs/ddmuilib/src/resources/images/down.png new file mode 100644 index 0000000000000000000000000000000000000000..f9426cbaab9a4fb6d23f0a3929377ec81e260bf8 GIT binary patch literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`A)YRdAr`&K2@)9xI=r1+U7y+; z{XaL?I^wF)L*u_d22c6BI|pAg?O_vOZfj)JIU)09!g1AhZMTH34@ouC1WZ~Mq}8+u ntd*RtsFLkw){=9!gqgu;pTXXo`MakB&1dj*^>bP0l+XkK2P-hw literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/e.png b/ddms/libs/ddmuilib/src/resources/images/e.png new file mode 100644 index 0000000000000000000000000000000000000000..dee7c97f87bd059e55b6667ad5deefd5ba2a7bc9 GIT binary patch literal 511 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU_9pO;uumfC+W}s|MtvQ2`MQ@ zL{btGegJvOyCi_h{{K&EGW_@F=V2XX<B-6eAkq6t8U#>dC|Z(9CC2N7rn>YVA zHZb_Z4p9LP4`pTLEQ<$R%xr9IZEb9A_Vxb^Hf-8t?(m zDx?{VTV~Gu`Txnu$%VgW0$JuhpMlbn5NRjF2lgt@ecN`j$@&BB{|yYcGbdg_Jk8AD W-^%N+vot3gq}J2b&t;ucLK6V>uHST5WJ6AArqvrOC+lFDWbF~vI0ARzyfB-3KT9ArAVI&DJ3*aknG?T5e6Ge$l@g1 z|F8S^z5FJOQ1Aok4S-^dd1ZH92LKdIgP&bHBgUaH{Odczw&&V63Vd&7a+(S#HFics zh(iI{kDLG&&gYA^?Y5LsAR;3+D;PI+Xg((?cS002ovPDHLkV1gh}apwR4 literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/file.png b/ddms/libs/ddmuilib/src/resources/images/file.png new file mode 100644 index 0000000000000000000000000000000000000000..043a81436d2df71bda1d1d73aa1a8f5e0b9219bf GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`DV{ElAr`%FCm-Z$FyL|iedPcD zrxmv@Y_VDu&TQSYO@CpIpnU7yt8dOTXc{+65OJ7w(@T2B@fVs;;}}gxrIs@9Nc`fKkno@%4l%eFh`vwVO_G4r?{9w)`0{A236Id1?3Ek3_rMd V#1?+MuoY+;gQu&X%Q~loCIAZmB?SNg literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/forward.png b/ddms/libs/ddmuilib/src/resources/images/forward.png new file mode 100644 index 0000000000000000000000000000000000000000..a97a605627e298cddc78e2300e13284ada6330b4 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`0iG_7Ar`%BFCXM$P~cz==>7M9 z`fIfyp1WDkzbOT6?vM03$@t*8qgaNsXn<2JgOg6dSGK@You@+-7BPKnSt%CDP?yX) krMacyo5pjG(_iNBR`24Fv-#?14m6v=)78&qol`;+018wtxBvhE literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/gc.png b/ddms/libs/ddmuilib/src/resources/images/gc.png new file mode 100644 index 0000000000000000000000000000000000000000..51948064fa9ce4f1f45be46fa76bdc94cae2d3da GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ii4<#Ar`&K2@3=ageLs=_t4d2 z{`Xkm_=UqwzMud9|6g19DlO$te-4M4_=C-|&hlpB26HS5nU?!;b90N>3G21q-j;jV zYcca2HUVxo-UG=E)2EBa+3`H^Id$U_lL_yF7YC=$+_2QcX=Z;31A_~zM3>}$#_2$N O89ZJ6T-G@yGywqK@;JKy literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/halt.png b/ddms/libs/ddmuilib/src/resources/images/halt.png new file mode 100644 index 0000000000000000000000000000000000000000..10e3720ae78844df90ffc6e4ff194630d41afd1c GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`J)SO(Ar`$$CkOH!HsEmne(mmR z^XF-kzi-^4dbsnJsHVI_j`HLW28y2R_yV%TV&3PTb#l5tzkyvv=Bsdm^P$ua{G9cT z!Y8%WRTr#(zNgcUJ7Kn(zv;{k>K|TzpB&i~=+luuyQ03=z3#7~XH*$KgWt>>2Lv}V w3wP*jySeQcf2`~^gAcZMHg!f6u3}n9jh-} z+yC$L4UK>zihgq#d4S;14buh8Z{idfADrU;Upwtb0!Low&8CIzkIuLlGBEs+FlT+e S?v@eI!3>_RelF{r5}E)#rCUb; literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/hprof.png b/ddms/libs/ddmuilib/src/resources/images/hprof.png new file mode 100644 index 0000000000000000000000000000000000000000..123d062071a77d4eb7d4884d7e4f49dc05d86f4e GIT binary patch literal 317 zcmV-D0mA-?P)j@Y+K{ps+VFrnIL1JS84nY^hHG*xYgp`|Os6tFODGeh4rr3rNvXtX zOb~^KrrB^d+SmUx@R@5hHSyDvlTfuy%bD5O+Wzma|L@lM`tSGe|JUD-|6l+A?_Z5a zK=6O&%$fE7|NWgU2{xr^c{!WQt3$K7jvhVw-`K$5&;9-N`~Lz14kQh9PLni)aSPCY z|DT+kY-p06p8mi6)c>~3f1sEED{wM=V6XDrw{0hztp7Y4%iqADI&gxrIs@9Nc`fJIh3SniH+f8qT`v+?cv3U7sd<^i z-?-3}$)Gt)QbIx^kVhfZcuqnC8_&vFff77FE;AV^%&=;gU|`T~RH}A1Uj($2fx*+& K&t;ucLK6V8cr#i6 literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/pause.png b/ddms/libs/ddmuilib/src/resources/images/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..19d286d3f6f28908d2c626a1ca6453f4d80e60bc GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`nw~C>Ar`&K2@)9xI?nt&f8c|> uXVb(97R*2(yi`xZG~r6a!Ndaxm>HNput7M9 z`r8|PO1HC)C+I8n{+QNPly#Gd^SPLUgRz5yjDdsH1&40E#0N(F4HXPFiMw5SCH57v lbvV9aOq`N`CbjAu7xV6oo&BC~Mz@xf4>WEEY@CO7J9=O4W^_15W}HKMTqdF)(#PN~TOaLs&t@9f(~g zaY{FiWa(J<-gD2DuSJ^W1-C+)<%M5z1u&g2Ta$+`eIU?Rp@|QuQ1(sY;Q7gLbv^;$ zbuM?Em&9wOMSL8XEmoDzY7!t`k4|S!0qsJF6KY~?lf;z~73>eK4s>UeZE+3JJ>GdP zfqL;@Xb8*}E2i^x>&y)o)Ly|x5-*JtmtdOh56%%ZLK#&GLm3guh|4|%?q_@(ec=7> z3GbsFqSro7!+~84F#a6#Afx{a)X^!=X1jC7oqJ&8`$}9;ek6^}_RH|I74i@~_U>tiaY`p$y+>T>J1R~fZM zPmEuqeXQWEz$Ryfi{%qjnpEoRx>6ccg!V=%TzuU0S1pa(v~%eZSxtt5?<^vw>!OyV zebau)HBD&2-}&<2R(Jhbk+CoPw#&3ku?3x3`l5`X=eOoslyaT^a;7jWxxLAG_40zQ cB?oF4isuCN)l5H833N7tr>mdKI;Vst0Jr&DJpcdz literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/save.png b/ddms/libs/ddmuilib/src/resources/images/save.png new file mode 100644 index 0000000000000000000000000000000000000000..040ebda68405a6161c33e216fa3c5c44a8e4d3af GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`+dW+zLo9le6BY;A`P>~F`wFzvP0HD1%J5kN06c)I$ztaD0e0suw3U;+RD literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/thread.png b/ddms/libs/ddmuilib/src/resources/images/thread.png new file mode 100644 index 0000000000000000000000000000000000000000..ac839e89dc739ec54cae63c0a39492df66c7e578 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`4xTQKAr`&K2@4p1_&4;&fBp{y zlan}tSjhcrzToL=>67sG1BDH+RVIl5H03eJ9#k+4Wh?TKhkqu~)IVFu$i$8&Od S0p>un7(8A5T-G@yGywpH11Hx2 literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/tracing_start.png b/ddms/libs/ddmuilib/src/resources/images/tracing_start.png new file mode 100644 index 0000000000000000000000000000000000000000..88771cc6b44ea4332fa572bd746ed587fb7bff58 GIT binary patch literal 227 zcmV<90382`P)PuAe#;3vzSgrM6KaL zbU;Oh*iOt$a3>9JR{=i>xu8Y}V4r@>#%hdSSI%c1;}e;7zmJdpxU$xSQ1o~14Cr#J z*VJx9Hj_Y|?z-UwMAOX#$$g=KhnUO@8u2wD7@`DduDp~hSn~vaOO&Yj8~6EV5*;Fy d8PWU?GY@6NXq!To*5v>I002ovPDHLkV1h|hUey2q literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/tracing_stop.png b/ddms/libs/ddmuilib/src/resources/images/tracing_stop.png new file mode 100644 index 0000000000000000000000000000000000000000..71bd215f4191f4abcfd64adda7dfe0f96d8bfa00 GIT binary patch literal 217 zcmV;~04D#5P)NklnqB zAV)nYxl@P!00A`f?c`uYmO>D~x_pV+=KYm=h5a*rp>kQIDfv7_W+n<5>vJrIDeFZ5 zGsPR*e)B}8p-I7RA``_It5Rdj8=#iH-eAh^Mcv5BpSTz7lhE=jGc$DJKg>JnHm2*8>e^@O1TaS?83{1OSo0E(rht literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/v.png b/ddms/libs/ddmuilib/src/resources/images/v.png new file mode 100644 index 0000000000000000000000000000000000000000..804405150e2ef415041e38ae244254a505e512c5 GIT binary patch literal 587 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU=s3laSW-Lll15Re|u)Dgp`yc zA}I+8KY%>tT@pZL|Nkd78UFk8^RSMx@@I) zYcF*%7e3~HZ`#>#J0w^80W;Xe2|ypj$H(_yR&z>%=u0`XkdeWF=di)^#y|emZtFi+ zxU(K{y?R&FpmUY6a>8x>3Wy6@*xAye8jcGYNyqX+M3R7poj7^&pn-$0Z*Ll@Nk%O*i3&ffVTjg9yB*Z&t67yp0$z=03-z+n6avIpX(CTRxa7GSvje{yoNp~>Mt zXU?4YBfab=Pyr7_fs^3_dzI(DZhST=HkF^AyaAeJth|~XY&bP0l+XkKW$_aL literal 0 HcmV?d00001 diff --git a/ddms/libs/ddmuilib/src/resources/images/w.png b/ddms/libs/ddmuilib/src/resources/images/w.png new file mode 100644 index 0000000000000000000000000000000000000000..129d0f9c22d49f3602e9119c0bc8227e1cea6fd4 GIT binary patch literal 681 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU@Gu*aSW-Lll15Re|u)Dgp`yc zA}I+8KY%>tT@pZL|Nkd78UFk8^RSMx@@I)^KFmIQAYlqqVVs~cGqe1amAoE$`3Wfr57HA(?3``O3eo0Z z^!ES%sex1{ibLW2k-#_nR{dR2^d5EeeRte@a8XgA>4DQ@uNc5I) zcwsqBpo;lOsp@WpE~v|r5)u+JGBO%G{sBWEAq|LAQW9p!a|oWc;9t1EzDeRwKV#Fv zi~L{{o7{nJVrF)}BE(Ufs>;l)KB?QzIn>C=$Y6s35Ewe$;Njr`ss#BAWblcTCmS`c zbhllw<_g>^!f(36TPEvb+K#zMN@iG~mW*CEYq&TE?M&^0wlQ`3t|4v$fgR|1Fh>HphL{cp?s2TDL- zcRCq9uvdBR+qRQU)_5A5k6L&Cu#Cn6rVdLD!#zw~YhyYtLgB3 \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=draw9patch.jar +frameworkdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + +if [ "$OSTYPE" = "cygwin" ] ; then + jarpath=`cygpath -w "$frameworkdir/$jarfile"` + progdir=`cygpath -w "$progdir"` +else + jarpath="$frameworkdir/$jarfile" +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +# might need more memory, e.g. -Xmx128M +exec java -Djava.ext.dirs="$frameworkdir" -jar "$jarpath" "$@" diff --git a/draw9patch/etc/draw9patch.bat b/draw9patch/etc/draw9patch.bat new file mode 100755 index 000000000..b6826fcdc --- /dev/null +++ b/draw9patch/etc/draw9patch.bat @@ -0,0 +1,46 @@ +@echo off +rem Copyright (C) 2008 The Android Open Source Project +rem +rem Licensed under the Apache License, Version 2.0 (the "License"); +rem you may not use this file except in compliance with the License. +rem You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem don't modify the caller's environment +setlocal + +rem Set up prog to be the path of this script, including following symlinks, +rem and set up progdir to be the fully-qualified pathname of its directory. +set prog=%~f0 + +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 + +rem Check we have a valid Java.exe in the path. +set java_exe= +call lib\find_java.bat +if not defined java_exe goto :EOF + +set jarfile=draw9patch.jar +set frameworkdir= +set libdir= + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=lib\ + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=..\framework\ + +:JarFileOk + +set jarpath=%frameworkdir%%jarfile% + +call %java_exe% -Djava.ext.dirs=%frameworkdir% -jar %jarpath% %* diff --git a/draw9patch/etc/manifest.txt b/draw9patch/etc/manifest.txt new file mode 100644 index 000000000..b2e3528ba --- /dev/null +++ b/draw9patch/etc/manifest.txt @@ -0,0 +1,2 @@ +Main-Class: com.android.draw9patch.Application +Class-Path: swing-worker-1.1.jar diff --git a/draw9patch/src/Android.mk b/draw9patch/src/Android.mk new file mode 100644 index 000000000..3dc9db4ea --- /dev/null +++ b/draw9patch/src/Android.mk @@ -0,0 +1,26 @@ +# 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAVA_RESOURCE_DIRS := resources + +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + swing-worker-1.1 +LOCAL_MODULE := draw9patch + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/draw9patch/src/com/android/draw9patch/Application.java b/draw9patch/src/com/android/draw9patch/Application.java new file mode 100644 index 000000000..68c792ab7 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/Application.java @@ -0,0 +1,55 @@ +/* + * 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.draw9patch; + +import com.android.draw9patch.ui.MainFrame; + +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; + +public class Application { + private static void initUserInterface() { + System.setProperty("apple.laf.useScreenMenuBar", "true"); + System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Draw 9-patch"); + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (UnsupportedLookAndFeelException e) { + e.printStackTrace(); + } + } + + public static void main(final String... args) { + initUserInterface(); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + String arg = args.length > 0 ? args[0] : null; + MainFrame frame = new MainFrame(arg); + frame.setDefaultCloseOperation(MainFrame.EXIT_ON_CLOSE); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + }); + } +} diff --git a/draw9patch/src/com/android/draw9patch/graphics/GraphicsUtilities.java b/draw9patch/src/com/android/draw9patch/graphics/GraphicsUtilities.java new file mode 100644 index 000000000..c6c182cee --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/graphics/GraphicsUtilities.java @@ -0,0 +1,96 @@ +/* + * 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.draw9patch.graphics; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsEnvironment; +import java.awt.Graphics; +import java.awt.Transparency; +import java.net.URL; +import java.io.IOException; + +public class GraphicsUtilities { + public static BufferedImage loadCompatibleImage(URL resource) throws IOException { + BufferedImage image = ImageIO.read(resource); + return toCompatibleImage(image); + } + + public static BufferedImage createCompatibleImage(int width, int height) { + return getGraphicsConfiguration().createCompatibleImage(width, height); + } + + public static BufferedImage toCompatibleImage(BufferedImage image) { + if (isHeadless()) { + return image; + } + + if (image.getColorModel().equals(getGraphicsConfiguration().getColorModel())) { + return image; + } + + BufferedImage compatibleImage = getGraphicsConfiguration().createCompatibleImage( + image.getWidth(), image.getHeight(), image.getTransparency()); + Graphics g = compatibleImage.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + + return compatibleImage; + } + + public static BufferedImage createCompatibleImage(BufferedImage image, int width, int height) { + return getGraphicsConfiguration().createCompatibleImage(width, height, + image.getTransparency()); + } + + private static GraphicsConfiguration getGraphicsConfiguration() { + GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + return environment.getDefaultScreenDevice().getDefaultConfiguration(); + } + + private static boolean isHeadless() { + return GraphicsEnvironment.isHeadless(); + } + + public static BufferedImage createTranslucentCompatibleImage(int width, int height) { + return getGraphicsConfiguration().createCompatibleImage(width, height, + Transparency.TRANSLUCENT); + } + + public static int[] getPixels(BufferedImage img, int x, int y, int w, int h, int[] pixels) { + if (w == 0 || h == 0) { + return new int[0]; + } + + if (pixels == null) { + pixels = new int[w * h]; + } else if (pixels.length < w * h) { + throw new IllegalArgumentException("Pixels array must have a length >= w * h"); + } + + int imageType = img.getType(); + if (imageType == BufferedImage.TYPE_INT_ARGB || imageType == BufferedImage.TYPE_INT_RGB) { + Raster raster = img.getRaster(); + return (int[]) raster.getDataElements(x, y, w, h, pixels); + } + + // Unmanages the image + return img.getRGB(x, y, w, h, pixels, 0, w); + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/GradientPanel.java b/draw9patch/src/com/android/draw9patch/ui/GradientPanel.java new file mode 100644 index 000000000..bc1465f1a --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/GradientPanel.java @@ -0,0 +1,47 @@ +/* + * 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.draw9patch.ui; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.GradientPaint; +import java.awt.Color; +import java.awt.Rectangle; +import java.awt.BorderLayout; +import javax.swing.JPanel; + +class GradientPanel extends JPanel { + private static final int DARK_BLUE = 0x202737; + + GradientPanel() { + super(new BorderLayout()); + } + + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g; + Rectangle clip = g2.getClipBounds(); + Paint paint = g2.getPaint(); + + g2.setPaint(new GradientPaint(0.0f, getHeight() * 0.22f, new Color(DARK_BLUE), + 0.0f, getHeight() * 0.9f, Color.BLACK)); + g2.fillRect(clip.x, clip.y, clip.width, clip.height); + + g2.setPaint(paint); + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/ImageEditorPanel.java b/draw9patch/src/com/android/draw9patch/ui/ImageEditorPanel.java new file mode 100644 index 000000000..84b96a538 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/ImageEditorPanel.java @@ -0,0 +1,1180 @@ +/* + * 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.draw9patch.ui; + +import com.android.draw9patch.graphics.GraphicsUtilities; + +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.BorderFactory; +import javax.swing.JSlider; +import javax.swing.JComponent; +import javax.swing.JScrollPane; +import javax.swing.JCheckBox; +import javax.swing.Box; +import javax.swing.JFileChooser; +import javax.swing.JSplitPane; +import javax.swing.JButton; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeListener; +import javax.swing.event.ChangeEvent; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.awt.Graphics2D; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Dimension; +import java.awt.TexturePaint; +import java.awt.Shape; +import java.awt.BasicStroke; +import java.awt.RenderingHints; +import java.awt.Rectangle; +import java.awt.GridBagLayout; +import java.awt.GridBagConstraints; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.AWTEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.AWTEventListener; +import java.awt.geom.Rectangle2D; +import java.awt.geom.Line2D; +import java.awt.geom.Area; +import java.awt.geom.RoundRectangle2D; +import java.io.IOException; +import java.io.File; +import java.net.URL; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +class ImageEditorPanel extends JPanel { + private static final String EXTENSION_9PATCH = ".9.png"; + private static final int DEFAULT_ZOOM = 8; + private static final float DEFAULT_SCALE = 2.0f; + + private String name; + private BufferedImage image; + private boolean is9Patch; + + private ImageViewer viewer; + private StretchesViewer stretchesViewer; + private JLabel xLabel; + private JLabel yLabel; + + private TexturePaint texture; + + private List patches; + private List horizontalPatches; + private List verticalPatches; + private List fixed; + private boolean verticalStartWithPatch; + private boolean horizontalStartWithPatch; + + private Pair horizontalPadding; + private Pair verticalPadding; + + ImageEditorPanel(MainFrame mainFrame, BufferedImage image, String name) { + this.image = image; + this.name = name; + + setTransferHandler(new ImageTransferHandler(mainFrame)); + + checkImage(); + + setOpaque(false); + setLayout(new BorderLayout()); + + loadSupport(); + buildImageViewer(); + buildStatusPanel(); + } + + private void loadSupport() { + try { + URL resource = getClass().getResource("/images/checker.png"); + BufferedImage checker = GraphicsUtilities.loadCompatibleImage(resource); + texture = new TexturePaint(checker, new Rectangle2D.Double(0, 0, + checker.getWidth(), checker.getHeight())); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void buildImageViewer() { + viewer = new ImageViewer(); + + JSplitPane splitter = new JSplitPane(); + splitter.setContinuousLayout(true); + splitter.setResizeWeight(0.8); + splitter.setBorder(null); + + JScrollPane scroller = new JScrollPane(viewer); + scroller.setOpaque(false); + scroller.setBorder(null); + scroller.getViewport().setBorder(null); + scroller.getViewport().setOpaque(false); + + splitter.setLeftComponent(scroller); + splitter.setRightComponent(buildStretchesViewer()); + + add(splitter); + } + + private JComponent buildStretchesViewer() { + stretchesViewer = new StretchesViewer(); + JScrollPane scroller = new JScrollPane(stretchesViewer); + scroller.setBorder(null); + scroller.getViewport().setBorder(null); + scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + scroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + return scroller; + } + + private void buildStatusPanel() { + JPanel status = new JPanel(new GridBagLayout()); + status.setOpaque(false); + + JLabel label = new JLabel(); + label.setForeground(Color.WHITE); + label.setText("Zoom: "); + label.putClientProperty("JComponent.sizeVariant", "small"); + status.add(label, new GridBagConstraints(0, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_END, GridBagConstraints.NONE, + new Insets(0, 6, 0, 0), 0, 0)); + + label = new JLabel(); + label.setForeground(Color.WHITE); + label.setText("100%"); + label.putClientProperty("JComponent.sizeVariant", "small"); + status.add(label, new GridBagConstraints(1, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_END, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + + JSlider zoomSlider = new JSlider(1, 16, DEFAULT_ZOOM); + zoomSlider.setSnapToTicks(true); + zoomSlider.putClientProperty("JComponent.sizeVariant", "small"); + zoomSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent evt) { + viewer.setZoom(((JSlider) evt.getSource()).getValue()); + } + }); + status.add(zoomSlider, new GridBagConstraints(2, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + + JLabel maxZoomLabel = new JLabel(); + maxZoomLabel.setForeground(Color.WHITE); + maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small"); + maxZoomLabel.setText("800%"); + status.add(maxZoomLabel, new GridBagConstraints(3, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + + label = new JLabel(); + label.setForeground(Color.WHITE); + label.setText("Patch scale: "); + label.putClientProperty("JComponent.sizeVariant", "small"); + status.add(label, new GridBagConstraints(0, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 6, 0, 0), 0, 0)); + + label = new JLabel(); + label.setForeground(Color.WHITE); + label.setText("2x"); + label.putClientProperty("JComponent.sizeVariant", "small"); + status.add(label, new GridBagConstraints(1, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_END, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + + zoomSlider = new JSlider(200, 600, (int) (DEFAULT_SCALE * 100.0f)); + zoomSlider.setSnapToTicks(true); + zoomSlider.putClientProperty("JComponent.sizeVariant", "small"); + zoomSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent evt) { + stretchesViewer.setScale(((JSlider) evt.getSource()).getValue() / 100.0f); + } + }); + status.add(zoomSlider, new GridBagConstraints(2, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + + maxZoomLabel = new JLabel(); + maxZoomLabel.setForeground(Color.WHITE); + maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small"); + maxZoomLabel.setText("6x"); + status.add(maxZoomLabel, new GridBagConstraints(3, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + + JCheckBox showLock = new JCheckBox("Show lock"); + showLock.setOpaque(false); + showLock.setForeground(Color.WHITE); + showLock.setSelected(true); + showLock.putClientProperty("JComponent.sizeVariant", "small"); + showLock.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + viewer.setLockVisible(((JCheckBox) event.getSource()).isSelected()); + } + }); + status.add(showLock, new GridBagConstraints(4, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 12, 0, 0), 0, 0)); + + JCheckBox showPatches = new JCheckBox("Show patches"); + showPatches.setOpaque(false); + showPatches.setForeground(Color.WHITE); + showPatches.putClientProperty("JComponent.sizeVariant", "small"); + showPatches.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + viewer.setPatchesVisible(((JCheckBox) event.getSource()).isSelected()); + } + }); + status.add(showPatches, new GridBagConstraints(4, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 12, 0, 0), 0, 0)); + + JCheckBox showPadding = new JCheckBox("Show content"); + showPadding.setOpaque(false); + showPadding.setForeground(Color.WHITE); + showPadding.putClientProperty("JComponent.sizeVariant", "small"); + showPadding.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + stretchesViewer.setPaddingVisible(((JCheckBox) event.getSource()).isSelected()); + } + }); + status.add(showPadding, new GridBagConstraints(5, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 12, 0, 0), 0, 0)); + + status.add(Box.createHorizontalGlue(), new GridBagConstraints(6, 0, 1, 1, 1.0f, 1.0f, + GridBagConstraints.LINE_START, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); + + label = new JLabel("X: "); + label.setForeground(Color.WHITE); + label.putClientProperty("JComponent.sizeVariant", "small"); + status.add(label, new GridBagConstraints(7, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_END, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + + xLabel = new JLabel("0px"); + xLabel.setForeground(Color.WHITE); + xLabel.putClientProperty("JComponent.sizeVariant", "small"); + status.add(xLabel, new GridBagConstraints(8, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_END, GridBagConstraints.NONE, + new Insets(0, 0, 0, 6), 0, 0)); + + label = new JLabel("Y: "); + label.setForeground(Color.WHITE); + label.putClientProperty("JComponent.sizeVariant", "small"); + status.add(label, new GridBagConstraints(7, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_END, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + + yLabel = new JLabel("0px"); + yLabel.setForeground(Color.WHITE); + yLabel.putClientProperty("JComponent.sizeVariant", "small"); + status.add(yLabel, new GridBagConstraints(8, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_END, GridBagConstraints.NONE, + new Insets(0, 0, 0, 6), 0, 0)); + + add(status, BorderLayout.SOUTH); + } + + private void checkImage() { + is9Patch = name.endsWith(EXTENSION_9PATCH); + if (!is9Patch) { + convertTo9Patch(); + } else { + ensure9Patch(); + } + } + + private void ensure9Patch() { + int width = image.getWidth(); + int height = image.getHeight(); + for (int i = 0; i < width; i++) { + int pixel = image.getRGB(i, 0); + if (pixel != 0 && pixel != 0xFF000000) { + image.setRGB(i, 0, 0); + } + pixel = image.getRGB(i, height - 1); + if (pixel != 0 && pixel != 0xFF000000) { + image.setRGB(i, height - 1, 0); + } + } + for (int i = 0; i < height; i++) { + int pixel = image.getRGB(0, i); + if (pixel != 0 && pixel != 0xFF000000) { + image.setRGB(0, i, 0); + } + pixel = image.getRGB(width - 1, i); + if (pixel != 0 && pixel != 0xFF000000) { + image.setRGB(width - 1, i, 0); + } + } + } + + private void convertTo9Patch() { + BufferedImage buffer = GraphicsUtilities.createTranslucentCompatibleImage( + image.getWidth() + 2, image.getHeight() + 2); + + Graphics2D g2 = buffer.createGraphics(); + g2.drawImage(image, 1, 1, null); + g2.dispose(); + + image = buffer; + name = name.substring(0, name.lastIndexOf('.')) + ".9.png"; + } + + File chooseSaveFile() { + if (is9Patch) { + return new File(name); + } else { + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(new PngFileFilter()); + int choice = chooser.showSaveDialog(this); + if (choice == JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + if (!file.getAbsolutePath().endsWith(EXTENSION_9PATCH)) { + String path = file.getAbsolutePath(); + if (path.endsWith(".png")) { + path = path.substring(0, path.lastIndexOf(".png")) + EXTENSION_9PATCH; + } else { + path = path + EXTENSION_9PATCH; + } + name = path; + is9Patch = true; + return new File(path); + } + is9Patch = true; + return file; + } + } + return null; + } + + RenderedImage getImage() { + return image; + } + + private class StretchesViewer extends JPanel { + private static final int MARGIN = 24; + + private StretchView horizontal; + private StretchView vertical; + private StretchView both; + + private Dimension size; + + private float horizontalPatchesSum; + private float verticalPatchesSum; + + private boolean showPadding; + + StretchesViewer() { + setOpaque(false); + setLayout(new GridBagLayout()); + setBorder(BorderFactory.createEmptyBorder(MARGIN, MARGIN, MARGIN, MARGIN)); + + horizontal = new StretchView(); + vertical = new StretchView(); + both = new StretchView(); + + setScale(DEFAULT_SCALE); + + add(vertical, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, + GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); + add(horizontal, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, + GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); + add(both, new GridBagConstraints(0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, + GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); + } + + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setPaint(texture); + g2.fillRect(0, 0, getWidth(), getHeight()); + g2.dispose(); + } + + void setScale(float scale) { + int patchWidth = image.getWidth() - 2; + int patchHeight = image.getHeight() - 2; + + int scaledWidth = (int) (patchWidth * scale); + int scaledHeight = (int) (patchHeight * scale); + + horizontal.scaledWidth = scaledWidth; + vertical.scaledHeight = scaledHeight; + both.scaledWidth = scaledWidth; + both.scaledHeight = scaledHeight; + + size = new Dimension(scaledWidth, scaledHeight); + + computePatches(); + } + + void computePatches() { + boolean measuredWidth = false; + boolean endRow = true; + + int remainderHorizontal = 0; + int remainderVertical = 0; + + if (fixed.size() > 0) { + int start = fixed.get(0).y; + for (Rectangle rect : fixed) { + if (rect.y > start) { + endRow = true; + measuredWidth = true; + } + if (!measuredWidth) { + remainderHorizontal += rect.width; + } + if (endRow) { + remainderVertical += rect.height; + endRow = false; + start = rect.y; + } + } + } + + horizontal.remainderHorizontal = horizontal.scaledWidth - remainderHorizontal; + vertical.remainderHorizontal = vertical.scaledWidth - remainderHorizontal; + both.remainderHorizontal = both.scaledWidth - remainderHorizontal; + + horizontal.remainderVertical = horizontal.scaledHeight - remainderVertical; + vertical.remainderVertical = vertical.scaledHeight - remainderVertical; + both.remainderVertical = both.scaledHeight - remainderVertical; + + horizontalPatchesSum = 0; + if (horizontalPatches.size() > 0) { + int start = -1; + for (Rectangle rect : horizontalPatches) { + if (rect.x > start) { + horizontalPatchesSum += rect.width; + start = rect.x; + } + } + } else { + int start = -1; + for (Rectangle rect : patches) { + if (rect.x > start) { + horizontalPatchesSum += rect.width; + start = rect.x; + } + } + } + + verticalPatchesSum = 0; + if (verticalPatches.size() > 0) { + int start = -1; + for (Rectangle rect : verticalPatches) { + if (rect.y > start) { + verticalPatchesSum += rect.height; + start = rect.y; + } + } + } else { + int start = -1; + for (Rectangle rect : patches) { + if (rect.y > start) { + verticalPatchesSum += rect.height; + start = rect.y; + } + } + } + + setSize(size); + ImageEditorPanel.this.validate(); + repaint(); + } + + void setPaddingVisible(boolean visible) { + showPadding = visible; + repaint(); + } + + private class StretchView extends JComponent { + private final Color PADDING_COLOR = new Color(0.37f, 0.37f, 1.0f, 0.5f); + + int scaledWidth; + int scaledHeight; + + int remainderHorizontal; + int remainderVertical; + + StretchView() { + scaledWidth = image.getWidth(); + scaledHeight = image.getHeight(); + } + + @Override + protected void paintComponent(Graphics g) { + int x = (getWidth() - scaledWidth) / 2; + int y = (getHeight() - scaledHeight) / 2; + + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.translate(x, y); + + x = 0; + y = 0; + + if (patches.size() == 0) { + g.drawImage(image, 0, 0, scaledWidth, scaledHeight, null); + g2.dispose(); + return; + } + + int fixedIndex = 0; + int horizontalIndex = 0; + int verticalIndex = 0; + int patchIndex = 0; + + boolean hStretch; + boolean vStretch; + + float vWeightSum = 1.0f; + float vRemainder = remainderVertical; + + vStretch = verticalStartWithPatch; + while (y < scaledHeight - 1) { + hStretch = horizontalStartWithPatch; + + int height = 0; + float vExtra = 0.0f; + + float hWeightSum = 1.0f; + float hRemainder = remainderHorizontal; + + while (x < scaledWidth - 1) { + Rectangle r; + if (!vStretch) { + if (hStretch) { + r = horizontalPatches.get(horizontalIndex++); + float extra = r.width / horizontalPatchesSum; + int width = (int) (extra * hRemainder / hWeightSum); + hWeightSum -= extra; + hRemainder -= width; + g.drawImage(image, x, y, x + width, y + r.height, r.x, r.y, + r.x + r.width, r.y + r.height, null); + x += width; + } else { + r = fixed.get(fixedIndex++); + g.drawImage(image, x, y, x + r.width, y + r.height, r.x, r.y, + r.x + r.width, r.y + r.height, null); + x += r.width; + } + height = r.height; + } else { + if (hStretch) { + r = patches.get(patchIndex++); + vExtra = r.height / verticalPatchesSum; + height = (int) (vExtra * vRemainder / vWeightSum); + float extra = r.width / horizontalPatchesSum; + int width = (int) (extra * hRemainder / hWeightSum); + hWeightSum -= extra; + hRemainder -= width; + g.drawImage(image, x, y, x + width, y + height, r.x, r.y, + r.x + r.width, r.y + r.height, null); + x += width; + } else { + r = verticalPatches.get(verticalIndex++); + vExtra = r.height / verticalPatchesSum; + height = (int) (vExtra * vRemainder / vWeightSum); + g.drawImage(image, x, y, x + r.width, y + height, r.x, r.y, + r.x + r.width, r.y + r.height, null); + x += r.width; + } + + } + hStretch = !hStretch; + } + x = 0; + y += height; + if (vStretch) { + vWeightSum -= vExtra; + vRemainder -= height; + } + vStretch = !vStretch; + } + + if (showPadding) { + g.setColor(PADDING_COLOR); + g.fillRect(horizontalPadding.first, verticalPadding.first, + scaledWidth - horizontalPadding.first - horizontalPadding.second, + scaledHeight - verticalPadding.first - verticalPadding.second); + } + + g2.dispose(); + } + + @Override + public Dimension getPreferredSize() { + return size; + } + } + } + + private class ImageViewer extends JComponent { + private final Color CORRUPTED_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.7f); + private final Color LOCK_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.7f); + private final Color STRIPES_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.5f); + private final Color BACK_COLOR = new Color(0xc0c0c0); + private final Color HELP_COLOR = new Color(0xffffe1); + private final Color PATCH_COLOR = new Color(1.0f, 0.37f, 0.99f, 0.5f); + private final Color PATCH_ONEWAY_COLOR = new Color(0.37f, 1.0f, 0.37f, 0.5f); + + private static final float STRIPES_WIDTH = 4.0f; + private static final double STRIPES_SPACING = 6.0; + private static final int STRIPES_ANGLE = 45; + + private int zoom; + private boolean showPatches; + private boolean showLock = true; + + private Dimension size; + + private boolean locked; + + private int[] row; + private int[] column; + + private int lastPositionX; + private int lastPositionY; + private int currentButton; + private boolean showCursor; + + private JLabel helpLabel; + private boolean eraseMode; + + private JButton checkButton; + private List corruptedPatches; + private boolean showBadPatches; + + private JPanel helpPanel; + + ImageViewer() { + setLayout(new GridBagLayout()); + helpPanel = new JPanel(new BorderLayout()); + helpPanel.setBorder(new EmptyBorder(0, 6, 0, 6)); + helpPanel.setBackground(HELP_COLOR); + helpLabel = new JLabel("Press Shift to erase pixels"); + helpLabel.putClientProperty("JComponent.sizeVariant", "small"); + helpPanel.add(helpLabel, BorderLayout.WEST); + checkButton = new JButton("Show bad patches"); + checkButton.putClientProperty("JComponent.sizeVariant", "small"); + checkButton.putClientProperty("JButton.buttonType", "roundRect"); + helpPanel.add(checkButton, BorderLayout.EAST); + + add(helpPanel, new GridBagConstraints(0, 0, 1, 1, + 1.0f, 1.0f, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(0, 0, 0, 0), 0, 0)); + + setOpaque(true); + + setZoom(DEFAULT_ZOOM); + findPatches(); + + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + // Store the button here instead of retrieving it again in MouseDragged + // below, because on linux, calling MouseEvent.getButton() for the drag + // event returns 0, which appears to be technically correct (no button + // changed state). + currentButton = event.isShiftDown() ? MouseEvent.BUTTON3 : event.getButton(); + paint(event.getX(), event.getY(), currentButton); + } + }); + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent event) { + if (!checkLockedRegion(event.getX(), event.getY())) { + // use the stored button, see note above + paint(event.getX(), event.getY(), currentButton); + } + } + + @Override + public void mouseMoved(MouseEvent event) { + checkLockedRegion(event.getX(), event.getY()); + } + }); + Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { + public void eventDispatched(AWTEvent event) { + enableEraseMode((KeyEvent) event); + } + }, AWTEvent.KEY_EVENT_MASK); + + checkButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + if (!showBadPatches) { + findBadPatches(); + checkButton.setText("Hide bad patches"); + } else { + checkButton.setText("Show bad patches"); + corruptedPatches = null; + } + repaint(); + showBadPatches = !showBadPatches; + } + }); + } + + private void findBadPatches() { + corruptedPatches = new ArrayList(); + + for (Rectangle patch : patches) { + if (corruptPatch(patch)) { + corruptedPatches.add(patch); + } + } + + for (Rectangle patch : horizontalPatches) { + if (corruptHorizontalPatch(patch)) { + corruptedPatches.add(patch); + } + } + + for (Rectangle patch : verticalPatches) { + if (corruptVerticalPatch(patch)) { + corruptedPatches.add(patch); + } + } + } + + private boolean corruptPatch(Rectangle patch) { + int[] pixels = GraphicsUtilities.getPixels(image, patch.x, patch.y, + patch.width, patch.height, null); + + if (pixels.length > 0) { + int reference = pixels[0]; + for (int pixel : pixels) { + if (pixel != reference) { + return true; + } + } + } + + return false; + } + + private boolean corruptHorizontalPatch(Rectangle patch) { + int[] reference = new int[patch.height]; + int[] column = new int[patch.height]; + reference = GraphicsUtilities.getPixels(image, patch.x, patch.y, + 1, patch.height, reference); + + for (int i = 1; i < patch.width; i++) { + column = GraphicsUtilities.getPixels(image, patch.x + i, patch.y, + 1, patch.height, column); + if (!Arrays.equals(reference, column)) { + return true; + } + } + + return false; + } + + private boolean corruptVerticalPatch(Rectangle patch) { + int[] reference = new int[patch.width]; + int[] row = new int[patch.width]; + reference = GraphicsUtilities.getPixels(image, patch.x, patch.y, + patch.width, 1, reference); + + for (int i = 1; i < patch.height; i++) { + row = GraphicsUtilities.getPixels(image, patch.x, patch.y + i, patch.width, 1, row); + if (!Arrays.equals(reference, row)) { + return true; + } + } + + return false; + } + + private void enableEraseMode(KeyEvent event) { + boolean oldEraseMode = eraseMode; + eraseMode = event.isShiftDown(); + if (eraseMode != oldEraseMode) { + if (eraseMode) { + helpLabel.setText("Release Shift to draw pixels"); + } else { + helpLabel.setText("Press Shift to erase pixels"); + } + } + } + + private void paint(int x, int y, int button) { + int color; + switch (button) { + case MouseEvent.BUTTON1: + color = 0xFF000000; + break; + case MouseEvent.BUTTON3: + color = 0; + break; + default: + return; + } + + int left = (getWidth() - size.width) / 2; + int top = (helpPanel.getHeight() + getHeight() - size.height) / 2; + + x = (x - left) / zoom; + y = (y - top) / zoom; + + int width = image.getWidth(); + int height = image.getHeight(); + if (((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) || + ((x > 0 && x < width - 1) && (y == 0 || y == height - 1))) { + image.setRGB(x, y, color); + findPatches(); + stretchesViewer.computePatches(); + if (showBadPatches) { + findBadPatches(); + } + repaint(); + } + } + + private boolean checkLockedRegion(int x, int y) { + int oldX = lastPositionX; + int oldY = lastPositionY; + lastPositionX = x; + lastPositionY = y; + + int left = (getWidth() - size.width) / 2; + int top = (helpPanel.getHeight() + getHeight() - size.height) / 2; + + x = (x - left) / zoom; + y = (y - top) / zoom; + + int width = image.getWidth(); + int height = image.getHeight(); + + xLabel.setText(Math.max(0, Math.min(x, width - 1)) + " px"); + yLabel.setText(Math.max(0, Math.min(y, height - 1)) + " px"); + + boolean previousLock = locked; + locked = x > 0 && x < width - 1 && y > 0 && y < height - 1; + + boolean previousCursor = showCursor; + showCursor = ((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) || + ((x > 0 && x < width - 1) && (y == 0 || y == height - 1)); + + if (locked != previousLock) { + repaint(); + } else if (showCursor || (showCursor != previousCursor)) { + Rectangle clip = new Rectangle(lastPositionX - 1 - zoom / 2, + lastPositionY - 1 - zoom / 2, zoom + 2, zoom + 2); + clip = clip.union(new Rectangle(oldX - 1 - zoom / 2, + oldY - 1 - zoom / 2, zoom + 2, zoom + 2)); + repaint(clip); + } + + return locked; + } + + @Override + protected void paintComponent(Graphics g) { + int x = (getWidth() - size.width) / 2; + int y = (helpPanel.getHeight() + getHeight() - size.height) / 2; + + Graphics2D g2 = (Graphics2D) g.create(); + g2.setColor(BACK_COLOR); + g2.fillRect(0, 0, getWidth(), getHeight()); + + g2.translate(x, y); + g2.setPaint(texture); + g2.fillRect(0, 0, size.width, size.height); + g2.scale(zoom, zoom); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g2.drawImage(image, 0, 0, null); + + if (showPatches) { + g2.setColor(PATCH_COLOR); + for (Rectangle patch : patches) { + g2.fillRect(patch.x, patch.y, patch.width, patch.height); + } + g2.setColor(PATCH_ONEWAY_COLOR); + for (Rectangle patch : horizontalPatches) { + g2.fillRect(patch.x, patch.y, patch.width, patch.height); + } + for (Rectangle patch : verticalPatches) { + g2.fillRect(patch.x, patch.y, patch.width, patch.height); + } + } + + if (corruptedPatches != null) { + g2.setColor(CORRUPTED_COLOR); + g2.setStroke(new BasicStroke(3.0f / zoom)); + for (Rectangle patch : corruptedPatches) { + g2.draw(new RoundRectangle2D.Float(patch.x - 2.0f / zoom, patch.y - 2.0f / zoom, + patch.width + 2.0f / zoom, patch.height + 2.0f / zoom, + 6.0f / zoom, 6.0f / zoom)); + } + } + + if (showLock && locked) { + int width = image.getWidth(); + int height = image.getHeight(); + + g2.setColor(LOCK_COLOR); + g2.fillRect(1, 1, width - 2, height - 2); + + g2.setColor(STRIPES_COLOR); + g2.translate(1, 1); + paintStripes(g2, width - 2, height - 2); + g2.translate(-1, -1); + } + + g2.dispose(); + + if (showCursor) { + Graphics cursor = g.create(); + cursor.setXORMode(Color.WHITE); + cursor.setColor(Color.BLACK); + cursor.drawRect(lastPositionX - zoom / 2, lastPositionY - zoom / 2, zoom, zoom); + cursor.dispose(); + } + } + + private void paintStripes(Graphics2D g, int width, int height) { + //draws pinstripes at the angle specified in this class + //and at the given distance apart + Shape oldClip = g.getClip(); + Area area = new Area(new Rectangle(0, 0, width, height)); + if(oldClip != null) { + area = new Area(oldClip); + } + area.intersect(new Area(new Rectangle(0,0,width,height))); + g.setClip(area); + + g.setStroke(new BasicStroke(STRIPES_WIDTH)); + + double hypLength = Math.sqrt((width * width) + + (height * height)); + + double radians = Math.toRadians(STRIPES_ANGLE); + g.rotate(radians); + + double spacing = STRIPES_SPACING; + spacing += STRIPES_WIDTH; + int numLines = (int)(hypLength / spacing); + + for (int i=0; i>> left = getPatches(column, result); + verticalStartWithPatch = result[0]; + + result = new boolean[1]; + Pair>> top = getPatches(row, result); + horizontalStartWithPatch = result[0]; + + fixed = getRectangles(left.first, top.first); + patches = getRectangles(left.second, top.second); + + if (fixed.size() > 0) { + horizontalPatches = getRectangles(left.first, top.second); + verticalPatches = getRectangles(left.second, top.first); + } else { + if (top.first.size() > 0) { + horizontalPatches = new ArrayList(0); + verticalPatches = getVerticalRectangles(top.first); + } else if (left.first.size() > 0) { + horizontalPatches = getHorizontalRectangles(left.first); + verticalPatches = new ArrayList(0); + } else { + horizontalPatches = verticalPatches = new ArrayList(0); + } + } + + row = GraphicsUtilities.getPixels(image, 0, height - 1, width, 1, row); + column = GraphicsUtilities.getPixels(image, width - 1, 0, 1, height, column); + + top = getPatches(row, result); + horizontalPadding = getPadding(top.first); + + left = getPatches(column, result); + verticalPadding = getPadding(left.first); + } + + private List getVerticalRectangles(List> topPairs) { + List rectangles = new ArrayList(); + for (Pair top : topPairs) { + int x = top.first; + int width = top.second - top.first; + + rectangles.add(new Rectangle(x, 1, width, image.getHeight() - 2)); + } + return rectangles; + } + + private List getHorizontalRectangles(List> leftPairs) { + List rectangles = new ArrayList(); + for (Pair left : leftPairs) { + int y = left.first; + int height = left.second - left.first; + + rectangles.add(new Rectangle(1, y, image.getWidth() - 2, height)); + } + return rectangles; + } + + private Pair getPadding(List> pairs) { + if (pairs.size() == 0) { + return new Pair(0, 0); + } else if (pairs.size() == 1) { + if (pairs.get(0).first == 1) { + return new Pair(pairs.get(0).second - pairs.get(0).first, 0); + } else { + return new Pair(0, pairs.get(0).second - pairs.get(0).first); + } + } else { + int index = pairs.size() - 1; + return new Pair(pairs.get(0).second - pairs.get(0).first, + pairs.get(index).second - pairs.get(index).first); + } + } + + private List getRectangles(List> leftPairs, + List> topPairs) { + List rectangles = new ArrayList(); + for (Pair left : leftPairs) { + int y = left.first; + int height = left.second - left.first; + for (Pair top : topPairs) { + int x = top.first; + int width = top.second - top.first; + + rectangles.add(new Rectangle(x, y, width, height)); + } + } + return rectangles; + } + + private Pair>> getPatches(int[] pixels, boolean[] startWithPatch) { + int lastIndex = 1; + int lastPixel = pixels[1]; + boolean first = true; + + List> fixed = new ArrayList>(); + List> patches = new ArrayList>(); + + for (int i = 1; i < pixels.length - 1; i++) { + int pixel = pixels[i]; + if (pixel != lastPixel) { + if (lastPixel == 0xFF000000) { + if (first) startWithPatch[0] = true; + patches.add(new Pair(lastIndex, i)); + } else { + fixed.add(new Pair(lastIndex, i)); + } + first = false; + + lastIndex = i; + lastPixel = pixel; + } + } + if (lastPixel == 0xFF000000) { + if (first) startWithPatch[0] = true; + patches.add(new Pair(lastIndex, pixels.length - 1)); + } else { + fixed.add(new Pair(lastIndex, pixels.length - 1)); + } + + if (patches.size() == 0) { + patches.add(new Pair(1, pixels.length - 1)); + startWithPatch[0] = true; + fixed.clear(); + } + + return new Pair>>(fixed, patches); + } + + void setLockVisible(boolean visible) { + showLock = visible; + repaint(); + } + } + + static class Pair { + E first; + E second; + + Pair(E first, E second) { + this.first = first; + this.second = second; + } + + @Override + public String toString() { + return "Pair[" + first + ", " + second + "]"; + } + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/ImageTransferHandler.java b/draw9patch/src/com/android/draw9patch/ui/ImageTransferHandler.java new file mode 100644 index 000000000..f14cd7764 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/ImageTransferHandler.java @@ -0,0 +1,86 @@ +/* + * 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.draw9patch.ui; + +import javax.swing.TransferHandler; +import javax.swing.JComponent; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.net.MalformedURLException; + +class ImageTransferHandler extends TransferHandler { + private final MainFrame mainFrame; + + ImageTransferHandler(MainFrame mainFrame) { + this.mainFrame = mainFrame; + } + + @Override + public boolean importData(JComponent component, Transferable transferable) { + try { + for (DataFlavor flavor : transferable.getTransferDataFlavors()) { + if (flavor.isFlavorJavaFileListType()) { + Object data = transferable.getTransferData(DataFlavor.javaFileListFlavor); + //noinspection unchecked + final File file = ((List) data).get(0); + mainFrame.open(file).execute(); + return true; + } else if (flavor.isFlavorTextType()) { + if (flavor.getRepresentationClass() == String.class) { + String mime = flavor.getMimeType(); + DataFlavor flave = new DataFlavor(mime); + Object data = transferable.getTransferData(flave); + final String path = convertPath(data.toString()); + mainFrame.open(new File(path)).execute(); + return true; + } + } + } + } catch (UnsupportedFlavorException e) { + // Ignore + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + + return false; + } + + private static String convertPath(String path) { + if (path.startsWith("file://")) path = path.substring("file://".length()); + if (path.indexOf('\n') != -1) path = path.substring(0, path.indexOf('\n')); + if (path.indexOf('\r') != -1) path = path.substring(0, path.indexOf('\r')); + return path; + } + + @Override + public boolean canImport(JComponent component, DataFlavor[] dataFlavors) { + for (DataFlavor flavor : dataFlavors) { + if (flavor.isFlavorJavaFileListType() || flavor.isFlavorTextType()) { + return true; + } + } + return false; + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/MainFrame.java b/draw9patch/src/com/android/draw9patch/ui/MainFrame.java new file mode 100644 index 000000000..d5b640918 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/MainFrame.java @@ -0,0 +1,174 @@ +/* + * 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.draw9patch.ui; + +import com.android.draw9patch.ui.action.ExitAction; +import com.android.draw9patch.ui.action.OpenAction; +import com.android.draw9patch.ui.action.SaveAction; +import com.android.draw9patch.graphics.GraphicsUtilities; + +import javax.swing.JFrame; +import javax.swing.JMenuBar; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.ActionMap; +import javax.swing.JFileChooser; +import javax.imageio.ImageIO; +import java.awt.HeadlessException; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.concurrent.ExecutionException; + +import org.jdesktop.swingworker.SwingWorker; + +public class MainFrame extends JFrame { + private ActionMap actionsMap; + private JMenuItem saveMenuItem; + private ImageEditorPanel imageEditor; + + public MainFrame(String path) throws HeadlessException { + super("Draw 9-patch"); + + buildActions(); + buildMenuBar(); + buildContent(); + + if (path == null) { + showOpenFilePanel(); + } else { + try { + File file = new File(path); + BufferedImage img = GraphicsUtilities.loadCompatibleImage(file.toURI().toURL()); + showImageEditor(img, file.getAbsolutePath()); + } catch (Exception ex) { + showOpenFilePanel(); + } + } + + // pack(); + setSize(1024, 600); + } + + private void buildActions() { + actionsMap = new ActionMap(); + actionsMap.put(OpenAction.ACTION_NAME, new OpenAction(this)); + actionsMap.put(SaveAction.ACTION_NAME, new SaveAction(this)); + actionsMap.put(ExitAction.ACTION_NAME, new ExitAction(this)); + } + + private void buildMenuBar() { + JMenu fileMenu = new JMenu("File"); + JMenuItem openMenuItem = new JMenuItem(); + saveMenuItem = new JMenuItem(); + JMenuItem exitMenuItem = new JMenuItem(); + + openMenuItem.setAction(actionsMap.get(OpenAction.ACTION_NAME)); + fileMenu.add(openMenuItem); + + saveMenuItem.setAction(actionsMap.get(SaveAction.ACTION_NAME)); + saveMenuItem.setEnabled(false); + fileMenu.add(saveMenuItem); + + exitMenuItem.setAction(actionsMap.get(ExitAction.ACTION_NAME)); + fileMenu.add(exitMenuItem); + + JMenuBar menuBar = new JMenuBar(); + menuBar.add(fileMenu); + setJMenuBar(menuBar); + } + + private void buildContent() { + setContentPane(new GradientPanel()); + } + + private void showOpenFilePanel() { + add(new OpenFilePanel(this)); + } + + public SwingWorker open(File file) { + if (file == null) { + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(new PngFileFilter()); + int choice = chooser.showOpenDialog(this); + if (choice == JFileChooser.APPROVE_OPTION) { + return new OpenTask(chooser.getSelectedFile()); + } else { + return null; + } + } else { + return new OpenTask(file); + } + } + + void showImageEditor(BufferedImage image, String name) { + getContentPane().removeAll(); + imageEditor = new ImageEditorPanel(this, image, name); + add(imageEditor); + saveMenuItem.setEnabled(true); + validate(); + repaint(); + } + + public SwingWorker save() { + if (imageEditor == null) { + return null; + } + + File file = imageEditor.chooseSaveFile(); + return file != null ? new SaveTask(file) : null; + } + + private class SaveTask extends SwingWorker { + private final File file; + + SaveTask(File file) { + this.file = file; + } + + protected Boolean doInBackground() throws Exception { + try { + ImageIO.write(imageEditor.getImage(), "PNG", file); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + } + + private class OpenTask extends SwingWorker { + private final File file; + + OpenTask(File file) { + this.file = file; + } + + protected BufferedImage doInBackground() throws Exception { + return GraphicsUtilities.loadCompatibleImage(file.toURI().toURL()); + } + + @Override + protected void done() { + try { + showImageEditor(get(), file.getAbsolutePath()); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/OpenFilePanel.java b/draw9patch/src/com/android/draw9patch/ui/OpenFilePanel.java new file mode 100644 index 000000000..a444332a0 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/OpenFilePanel.java @@ -0,0 +1,51 @@ +package com.android.draw9patch.ui; + +import com.android.draw9patch.graphics.GraphicsUtilities; + +import javax.swing.JComponent; +import java.awt.image.BufferedImage; +import java.awt.Graphics; +import java.io.IOException; +import java.net.URL;/* + * 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. + */ + +class OpenFilePanel extends JComponent { + private BufferedImage dropHere; + + OpenFilePanel(MainFrame mainFrame) { + setOpaque(false); + loadSupportImage(); + setTransferHandler(new ImageTransferHandler(mainFrame)); + } + + private void loadSupportImage() { + try { + URL resource = getClass().getResource("/images/drop.png"); + dropHere = GraphicsUtilities.loadCompatibleImage(resource); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + protected void paintComponent(Graphics g) { + int x = (getWidth() - dropHere.getWidth()) / 2; + int y = (getHeight() - dropHere.getHeight()) / 2; + + g.drawImage(dropHere, x, y, null); + } + +} diff --git a/draw9patch/src/com/android/draw9patch/ui/PngFileFilter.java b/draw9patch/src/com/android/draw9patch/ui/PngFileFilter.java new file mode 100644 index 000000000..8f8885a74 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/PngFileFilter.java @@ -0,0 +1,32 @@ +/* + * 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.draw9patch.ui; + +import javax.swing.filechooser.FileFilter; +import java.io.File; + +class PngFileFilter extends FileFilter { + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".png"); + } + + @Override + public String getDescription() { + return "PNG Image (*.png)"; + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/action/BackgroundAction.java b/draw9patch/src/com/android/draw9patch/ui/action/BackgroundAction.java new file mode 100644 index 000000000..85d9d4fc6 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/action/BackgroundAction.java @@ -0,0 +1,29 @@ +/* + * 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.draw9patch.ui.action; + +import org.jdesktop.swingworker.SwingWorker; + +import javax.swing.AbstractAction; + +public abstract class BackgroundAction extends AbstractAction { + protected void executeBackgroundTask(SwingWorker worker) { + if (worker != null) { + worker.execute(); + } + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/action/ExitAction.java b/draw9patch/src/com/android/draw9patch/ui/action/ExitAction.java new file mode 100644 index 000000000..b6f047d91 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/action/ExitAction.java @@ -0,0 +1,44 @@ +/* + * 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.draw9patch.ui.action; + +import javax.swing.AbstractAction; +import javax.swing.KeyStroke; +import javax.swing.JFrame; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class ExitAction extends AbstractAction { + public static final String ACTION_NAME = "exit"; + private JFrame frame; + + public ExitAction(JFrame frame) { + putValue(NAME, "Quit"); + putValue(SHORT_DESCRIPTION, "Quit"); + putValue(LONG_DESCRIPTION, "Quit"); + putValue(MNEMONIC_KEY, KeyEvent.VK_Q); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.frame = frame; + } + + public void actionPerformed(ActionEvent e) { + frame.dispose(); + System.exit(0); + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/action/OpenAction.java b/draw9patch/src/com/android/draw9patch/ui/action/OpenAction.java new file mode 100644 index 000000000..45ee5be26 --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/action/OpenAction.java @@ -0,0 +1,43 @@ +/* + * 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.draw9patch.ui.action; + +import com.android.draw9patch.ui.MainFrame; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class OpenAction extends BackgroundAction { + public static final String ACTION_NAME = "open"; + private MainFrame frame; + + public OpenAction(MainFrame frame) { + this.frame = frame; + putValue(NAME, "Open 9-patch..."); + putValue(SHORT_DESCRIPTION, "Open..."); + putValue(LONG_DESCRIPTION, "Open 9-patch..."); + putValue(MNEMONIC_KEY, KeyEvent.VK_O); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_O, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(frame.open(null)); + } +} diff --git a/draw9patch/src/com/android/draw9patch/ui/action/SaveAction.java b/draw9patch/src/com/android/draw9patch/ui/action/SaveAction.java new file mode 100644 index 000000000..5c1dc526c --- /dev/null +++ b/draw9patch/src/com/android/draw9patch/ui/action/SaveAction.java @@ -0,0 +1,43 @@ +/* + * 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.draw9patch.ui.action; + +import com.android.draw9patch.ui.MainFrame; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class SaveAction extends BackgroundAction { + public static final String ACTION_NAME = "save"; + private MainFrame frame; + + public SaveAction(MainFrame frame) { + this.frame = frame; + putValue(NAME, "Save 9-patch..."); + putValue(SHORT_DESCRIPTION, "Save..."); + putValue(LONG_DESCRIPTION, "Save 9-patch..."); + putValue(MNEMONIC_KEY, KeyEvent.VK_S); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(frame.save()); + } +} diff --git a/draw9patch/src/resources/images/checker.png b/draw9patch/src/resources/images/checker.png new file mode 100644 index 0000000000000000000000000000000000000000..78908f47e14146da34693476dcbf864761b6cce5 GIT binary patch literal 1889 zcmeAS@N?(olHy`uVBq!ia0y~yV0-|=985rwPunyP0V&P`kH}&M25w;xW@MN(M*=9Q zkn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&791A}<}r1+z53=C|~JzX3_DsH{G zv$6NE0RxNU?}vR?>e+*Qi-~Zkx|NlenqP=_f{{8=M?gGY`LIxyxCf);IKJHxzl&vj( z&%*nliU|46t02FCt1In;S0qKNKa$7(eOGY)0 r2F_^88O&fNRQIrlzy?lVvPeIp$TG8Qrb04Q{!nx;3k_Madjy2(BD*N~e+ z3WwS}1^^J)WVWA$^bQOJOnz2*9<5k1e``)UDkp7B7hU$v#n_Wfj-L(#{=wMa?s-y zr2*`8?kG*bOA)Alr&SD7`b8P8#j9w3VWB(F(f008exF z>AJLA3*KHgp?#FBnU{1dS;`r&kuK2B^YPm4>#FcMfy$~3(BQ3t7oH|k;owT}Tc3~G zIuQ5u6abE}{_j8WlMo=hqP|Q9UmvJm>J;$+2oK%#M*!fc$t`L=(yTg20sxvtNWlhm z)|2;4!Z@Nk?{6)=2VFSHMr&~Q^=MFQkhvl51UL!QJlEil9d7^&I*EN!154vfykaxp zl%hQrZIsGzs=v-8h41-0kfem_LnL(091AEsmjr9BWD>^Qm@|&|P(s>7ZuVi+Z4*xY zG#RLbX@WVs1r&Uw9Dpw$XRrM(O>qd2e0~wzp(F7U*<_;piTo|3y+V#9TDutZ!7WfC zFNL8f|3g3vi*l0e>+KKKUQ`PyDv&{O+&3m-sXkwE(Scr##(Tn~8Lp#^oHgXt11t@e zH3ZoBU#^pl!GyF(EgY#VVXy|fGn+cp+Q8aq9$LYEhg+IvfmDu^Aljsdk29k}zlHZ5 z&7VFd7fFdYYp5{SENu#%7Yfr`3Z7ebWNc&Oyqn|0)j&NnZG?axa zV|c;Mr-@kpr7*kDP|OB;qjy7UgLQ-B$c_x{qV}ld%4>);^CtDC^d^yej-^Fyir3~eGAXtiwXZRz^(Ju@bka}q=QG+N?e&&28VkS@KV5a&UgEHpnMCL(raXE<}Ior8Zx&9W` zf*8T4^l2kxnR=PLnXH0h=9N`tRV!5>i(`vX^OQOCoJ96U57s*d*g>w$%^|`ZJ`~llPmnTwyp7|Z zp9TqHb1uQyMCGc=*YXf4Jt?bZ)8m&Os>JI#kIo7LjYzlo%d?Lo6IN~?2z(V_Et58A zmvFcDsmjXEvaW_!n^f!k=#;J7D@T`gYGG~8k(BC#dVKbLI-V&Lx6s>WpUsxb$;ufF zl@)%~iB|cPvcAS@26y|F@8=YDIU))T{L5EDJGcMdi4eOqzl8lQ|D{82P1H=zO|(cp z!KBnL^T=NbEH=3K(*)wss|G1`&UDWH5}npQrSxV-`-L`D(!&A!b?$Z80Bqn5FA*;X zufE}T!~Hx{!}cT$y}ji8;yb{O$a?{MA+`+Ynn}Oa-RRUAJk& zw#;T9)8Q9mD{oV3ZDH%stX+$)<(b9SV#{gD+l?}estad5dmDaYhB1|`%nh6KM2mG8 z7$(Bm(}mHqwZE=+Kch1v;$6gxOJ!n;xX+Z;U|VqH=x>13?hiJ3>X&p~S&-TYr$Ed`3{pX{i*(##>tOLtKof~?AzaBY!3H$g9 zo!^|_vsbZLzc0K0a6fM8)tK}vfkGRp#~*r!AZsR@Q>vg>nc_aHHIc>;W2EQXZ7wla zu{5jNxrWx#;Hp#S14fnKDgm$7Ro0c{RwP$!9*oK1T8COkjeGqIAH=Leo^f5fU58yy z0p$oDJWqmnq&m_L|NJcMDMxp*s=}iC@21at>PbcI>h$U<&ugD=zOUu*8rn^*NEvfK^4shOV&^UA#NJ49zL>Zet60@!ILex3N+Me(cxnmqcvbVSW|rnkzNdt- ze7%UCjG#)DsJGIB+&k%asso}PHe)l^Xlq?x{X6oaf_1WfzFn&qd&hBK(O*I9;fy(A zLk{rJ*AqP_G35-EqO`&aS!xC-qkAt8)u{z57?OC7=z@8Y$wGU;J)WFmQ?gU2d5FCr zo6!+e&@e20H?2b^l)GQ5RCu#Jny)go!+^qIIbWfWAuos>CaJ8@DG`;PmikDeio-Ai zg{TWG8Q!p%2pW{mAq8Z}EJ7fGo&1V+oZ=wkDaX*kwQDltg7s-Y)U_h&kHRStClT$ zzkX{iie-=UFJhxGkoGs-N^KqP<--%>imK)%<|pQV&1LsLF28l2JM){`XAST8ExkLl zXxDLisWRNPXn|*8IO8$ney)4oe5zU(U$t|Vbq;f=HG)0b8zlHha7oNXiJ5EvGz0#q zY;Kw|EA&H$a`0}~75X4pghH0W;zhy0S1!0_=*G?|>FOyPg~CEmQh3c5;_16VU{A1^ zw?^Jko_4-j-npHN-8@XpU&_+noL4xq9Yre-z$Rk(d9f8gRLDQ4X1Oo8&EIyehsLtz%>mi#(h8F*h-lGxa+AC_AGY_O$f! zJMIT_{o+8+09)5^h?IomiRJm;`{1Qu@WGo^D&vi(?9ceGz5X=*GW$?|p23`vaZN~& zb9~_{C9-(qHwit}4b=glG5P7aNyg2Z-4$wT2moO`0Dy=BfIrtabq4^Rhy%cPCjd~$ z0RTq-^auUgH@5V-uBN(q$iglfmPBs_9(Z;d@L2?^*AGzKwH3%LYx%&^0z>h?6?rSL z!06jz*=TE)u5Fmf?=3-{M3gvipQUO>TSTvj=Dw>CjLxf+23Eo8^Dq>|U>Du>!O8hx ze>wc%XRrK^Lh%0n#J91P-1bSa+}*<5$J+&eCZ#;D9y@{tn24Nji!-+k!RP>jmxyfS zU4V}{&jtrpWBe&WJ5T6K%6!eXcBhEN?C-52x-&MowC2SyP7a}$BskEvQ^K z)}3Q|kf~V=d4f%nnARv$72;s6Npy-DC*AzAkh)_`657T1LPGzAgc&Qkd|qwP%z^Yb z2|P*&#%l7U)Q{;qH{|89B^|VzPs2VVmBO2}p9kp!Vm-ijVm@SohaWl#P>~` z#6#R%*s;Um(**(C0W$bjzNv zC88YBs?m3sc+3!2uDtg2y|_J5K{mn|3l5QSi`=GQ-};H;!ENHk@Va!n))O&cYpNwC zR2z-;rX)`v{E%{u4i~T{?0+rbq;eQi@3l?9ilA{Uv8aRYr-QZV+VIapaTXD@ZS77; z%@OI3%r^MGnetg3-0%SCq;Q72N#|cp)z0YKa>AT#f(Rj(BUra02D{Wt* zHAg}ybE0AP{Ml|)X?R&{EDVUS_^SxeLZgVGMoIkKr5fqtg3LlpI0Dj?29@d*anYJ= zwHQZ2Wja-04rSCf>N2-9is}CYZ7p?gl5L+RLmRE4TQ#LQ2r?GVuE{bV4Q8hN9?mYg zG4T6cQz>l&&^EGHfloyp8CXu;i#(wGq(A>!7Sw?@)@$M1>#tLN`8q#jun&}m@`@R{ z{g?X3q%=_>wkG!_Qu&=SEsSfHVA?FFp3*7SFJ*~OzK5d&HgE=8)dfbqOaD!?C^zgj`l)(+v1B4vzP-qL>92m9(g=0KhNe14Z7?;L>^ zwa0VJc%hjmz~nk{sAm@i)^LQGgzplSm5Vf3D}I*3f`E8)Vezvk8Rth;^Z^FGMH^@9tpTwxVO$o%FeaPH&qo6z5?U<9^w-`4^it`qQ0#)?RVKJDYZ%Rw{o-t^Cos;-!+OiL%*u*=gA37mqH~^G@sH+pE5l4u9;x zYS(^v6?99!(ws_NU%IK~xtGsI$)5lc+cJ5qg|_I%ayP6pHq(;D@0k4XSApXqZEys% z<&Ud7<>g}558+P<1g!o9l-nD&l-M8Y#p7~==xeLtvnS?DPwx05!?&nFKA&|ngYT&x zt|Xl|D%x&DDibFSBoFL)Z*pu1HM)+fq`&XDp55%Wux1FPnmFvKm90GTL_nBFk2$gr zvPzwjatZ9e_E?>ncUT2iV7&cojZ--7L)C#e~spLZK;HH^v5j_fXr7Bc2U5 z@{zDLSAn5HBH!xlR4>S%)@MYbUM_6E@v}!oQoY{51rBxcq#nTfNiF@5n~(P+KOqw| z74HS(t9om^10jjsuda_2*TPP_r{d<p1=PZo<_r-oDt#~!K8q~H5ikGFks=cs|$5ih22W_Ex;Ft#8mSPT8 zl2xlB<_KnPTZy7)SRd3q?8cy8%fsTY7oU&{zOUeeLXw%9`Mi|k@X=X~V0&mw<%$0R z)h>2*kC7JUZ4-^&q&f{wGvNGZyt1&WEDbuDf0!*EPSQs5`4mNd!9JSVQj2W zEo;FC!r6#^TzMzD)x|29hV$)P;_?o--1in?N%ntrDb}u~ih%B6FQJy*Vb871F3uQ( zpE7oYj;BE_c++lNX`BQ2be(Wxnt2-337%7nFOO-DA?}`72Ns1^xkR--mW4;%?5Qap z>N=IQ9!zl_8t*Q==}K%M&k@a!=Y32C&t$xkJFMo!?ED_|(-AX6>L5;s{EBsXb_4iq zU){w&zjSm=MA9Rn*Tw!*t&DE^Y2sWB%Oolwd**wyNQc%xD6s|3hP;7M3NW!F5v9WQ zSASf3DswwH$M|~F;QDUDd>B>+N5}EiI51Q>o%JQz-k8;Wa>gy9F<^`5(AqCs)ibnbFK&CXY)1(mM6Rcsg?GDG;Wo;P)_LbDMj zf;{mdU5U#J&yQr9XL*Xx`cr@HkuQ3$7AJ%Bj;kYyw}N{7d0j{{Ajp>*7ZA-ld*hA- zX?#Cqs<9bqg}|#sukOn#qR_tka=7LfoEM&e`F!7TDa_XL#<>EzT1J|68cxsu57IC& Ae*gdg literal 0 HcmV?d00001 diff --git a/dumpeventlog/.classpath b/dumpeventlog/.classpath new file mode 100644 index 000000000..b0326c84a --- /dev/null +++ b/dumpeventlog/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dumpeventlog/.project b/dumpeventlog/.project new file mode 100644 index 000000000..c416f4f45 --- /dev/null +++ b/dumpeventlog/.project @@ -0,0 +1,17 @@ + + + dumpeventlog + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/dumpeventlog/Android.mk b/dumpeventlog/Android.mk new file mode 100644 index 000000000..7bb870dee --- /dev/null +++ b/dumpeventlog/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2007 The Android Open Source Project +# +DUMPEVENTLOG_LOCAL_DIR := $(call my-dir) +include $(DUMPEVENTLOG_LOCAL_DIR)/etc/Android.mk +include $(DUMPEVENTLOG_LOCAL_DIR)/src/Android.mk diff --git a/dumpeventlog/etc/Android.mk b/dumpeventlog/etc/Android.mk new file mode 100644 index 000000000..80947346f --- /dev/null +++ b/dumpeventlog/etc/Android.mk @@ -0,0 +1,8 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PREBUILT_EXECUTABLES := dumpeventlog +include $(BUILD_HOST_PREBUILT) + diff --git a/dumpeventlog/etc/dumpeventlog b/dumpeventlog/etc/dumpeventlog new file mode 100755 index 000000000..56f8c2229 --- /dev/null +++ b/dumpeventlog/etc/dumpeventlog @@ -0,0 +1,81 @@ +#!/bin/sh +# Copyright 2005-2007, 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. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=dumpeventlog.jar +frameworkdir="$progdir" +libdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + + +# Check args. +if [ debug = "$1" ]; then + # add this in for debugging + java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +else + java_debug= +fi + +# Mac OS X needs an additional arg, or you get an "illegal thread" complaint. +if [ `uname` = "Darwin" ]; then + os_opts="-XstartOnFirstThread" +else + os_opts= +fi + +if [ "$OSTYPE" = "cygwin" ] ; then + jarpath=`cygpath -w "$frameworkdir/$jarfile"` + progdir=`cygpath -w "$progdir"` +else + jarpath="$frameworkdir/$jarfile" +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +# might need more memory, e.g. -Xmx128M +exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Djava.library.path="$libdir" -jar "$jarpath" "$@" diff --git a/dumpeventlog/etc/manifest.txt b/dumpeventlog/etc/manifest.txt new file mode 100644 index 000000000..0eea91537 --- /dev/null +++ b/dumpeventlog/etc/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.dumpeventlog.DumpEventLog diff --git a/dumpeventlog/src/Android.mk b/dumpeventlog/src/Android.mk new file mode 100644 index 000000000..bf9937512 --- /dev/null +++ b/dumpeventlog/src/Android.mk @@ -0,0 +1,14 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + ddmlib +LOCAL_MODULE := dumpeventlog + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/dumpeventlog/src/com/android/dumpeventlog/DumpEventLog.java b/dumpeventlog/src/com/android/dumpeventlog/DumpEventLog.java new file mode 100644 index 000000000..695573c61 --- /dev/null +++ b/dumpeventlog/src/com/android/dumpeventlog/DumpEventLog.java @@ -0,0 +1,145 @@ +/* + * 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.dumpeventlog; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.ILogOutput; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.log.LogReceiver; +import com.android.ddmlib.log.LogReceiver.ILogListener; +import com.android.ddmlib.log.LogReceiver.LogEntry; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Connects to a device using ddmlib and dumps its event log as long as the device is connected. + */ +public class DumpEventLog { + + /** + * Custom {@link ILogListener} to receive and save the event log raw output. + */ + private static class LogWriter implements ILogListener { + private FileOutputStream mOutputStream; + private LogReceiver mReceiver; + + public LogWriter(String filePath) throws IOException { + mOutputStream = new FileOutputStream(filePath); + } + + public void newData(byte[] data, int offset, int length) { + try { + mOutputStream.write(data, offset, length); + } catch (IOException e) { + if (mReceiver != null) { + mReceiver.cancel(); + } + System.out.println(e); + } + } + + public void newEntry(LogEntry entry) { + // pass + } + + public void setReceiver(LogReceiver receiver) { + mReceiver = receiver; + } + + public void done() throws IOException { + mOutputStream.close(); + } + } + + public static void main(String[] args) { + if (args.length != 2) { + System.out.println("Usage: dumpeventlog "); + return; + } + + // redirect the log output to /dev/null + Log.setLogOutput(new ILogOutput() { + public void printAndPromptLog(LogLevel logLevel, String tag, String message) { + // pass + } + + public void printLog(LogLevel logLevel, String tag, String message) { + // pass + } + }); + + // init the lib + AndroidDebugBridge.init(false /* debugger support */); + + try { + AndroidDebugBridge bridge = AndroidDebugBridge.createBridge(); + + // we can't just ask for the device list right away, as the internal thread getting + // them from ADB may not be done getting the first list. + // Since we don't really want getDevices() to be blocking, we wait here manually. + int count = 0; + while (bridge.hasInitialDeviceList() == false) { + try { + Thread.sleep(100); + count++; + } catch (InterruptedException e) { + // pass + } + + // let's not wait > 10 sec. + if (count > 100) { + System.err.println("Timeout getting device list!"); + return; + } + } + + // now get the devices + IDevice[] devices = bridge.getDevices(); + + for (IDevice device : devices) { + if (device.getSerialNumber().equals(args[0])) { + try { + grabLogFrom(device, args[1]); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return; + } + } + + System.err.println("Could not find " + args[0]); + } finally { + AndroidDebugBridge.terminate(); + } + } + + private static void grabLogFrom(IDevice device, String filePath) throws IOException { + LogWriter writer = new LogWriter(filePath); + LogReceiver receiver = new LogReceiver(writer); + writer.setReceiver(receiver); + + device.runEventLogService(receiver); + + writer.done(); + } +} diff --git a/eclipse/README_WINDOWS.txt b/eclipse/README_WINDOWS.txt new file mode 100644 index 000000000..1480f5d7e --- /dev/null +++ b/eclipse/README_WINDOWS.txt @@ -0,0 +1,32 @@ +[RM 20080623] + +1- To build the Eclipse plugin: +Under Linux: +$ cd your-device-directory +$ tools/eclipse/scripts/build_server.sh destination-directory + +This will create an "android-eclipse.zip" in the selected destination directory. +Then in Eclipse, you can use Help > Software Updates > Find and Install > Search for new Features > Next > New Archived Site > select the new android-eclipse.zip. Then with the new archive checked, click Finish/Next. + + +2- To build a Windows SDK, you need two steps: +a- First you need to create a Linux SDK: + +Under Linux: +$ cd your-device-directory +$ make sdk +Note: if you get an error when building the javadoc, make sure you use a Java SDK 1.5 +Note: if you get an error when building layoutlib, make sure you use a Java SDK 1.5.0-b13. + +b- Once you have a Linux SDK, you can create a Windows SDK: + +You need a Windows machine with XP or Vista and Cygwin. +- Installer at http://sources.redhat.com/cygwin/ +- Set Default Text File Type to DOS/text, not Unix/binary. +- Select packages autoconf, gcc, g++, bison, python, zip, unzip, mingw-zlib +- Suggested extra packages: emacs, wget, openssh, rsync + +Then under Cygwin: +$ cd your-device-directory +$ tools/buildbot/_make_windows_sdk.sh path-to-the-linux-sdk.zip destination-directory + diff --git a/eclipse/buildConfig/allElements.xml b/eclipse/buildConfig/allElements.xml new file mode 100644 index 000000000..2c8229c28 --- /dev/null +++ b/eclipse/buildConfig/allElements.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse/buildConfig/build.properties b/eclipse/buildConfig/build.properties new file mode 100644 index 000000000..cd477d80b --- /dev/null +++ b/eclipse/buildConfig/build.properties @@ -0,0 +1,238 @@ +############################################################################### +# Copyright (c) 2003, 2006 IBM Corporation and others. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# Contributors: +# IBM Corporation - initial API and implementation +############################################################################### + + +# This file was generated per the instructions located in Eclipse Help>Plug-in Development +# Environment > Guide > Tasks > Building features and customized for building the +# Android Eclipse plugins. + +##################### +# Parameters describing how and where to execute the build. +# Typical users need only update the following properties: +# baseLocation - where things you are building against are installed +# bootclasspath - The base jars to compile against (typicaly rt.jar) +# configs - the list of {os, ws, arch} configurations to build. +# +# Of course any of the settings here can be overridden by spec'ing +# them on the command line (e.g., -DbaseLocation=d:/eclipse + +############# PRODUCT/PACKAGING CONTROL ############# +product=/plugin or feature id/path/to/.product +runPackager=true + +#Set the name of the archive that will result from the product build. +#archiveNamePrefix= + +# The prefix that will be used in the generated archive. +# override default of "eclipse" to aid for external site generation +archivePrefix=android-eclipse + +# The location underwhich all of the build output will be collected. +collectingFolder=${archivePrefix} + +# The list of {os, ws, arch} configurations to build. This +# value is a '&' separated list of ',' separate triples. For example, +# configs=win32,win32,x86 & linux,motif,x86 +# By default the value is *,*,* +configs = *, *, * +#configs=win32, win32, x86 & \ +# linux, gtk, ppc &\ +# linux, gtk, x86 & \ +# linux, gtk, x86_64 & \ +# linux, motif, x86 & \ +# solaris, motif, sparc & \ +# solaris, gtk, sparc & \ +# aix, motif, ppc & \ +# hpux, motif, PA_RISC & \ +# macosx, carbon, ppc + +# By default PDE creates one archive (result) per entry listed in the configs property. +# Setting this value to try will cause PDE to only create one output containing all +# artifacts for all the platforms listed in the configs property. +#groupConfigurations=true + +#The format of the archive. By default a zip is created using antZip. +#The list can only contain the configuration for which the desired format is different than zip. +#archivesFormat=win32, win32, x86 - antZip& \ +# linux, gtk, ppc - antZip &\ +# linux, gtk, x86 - antZip& \ +# linux, gtk, x86_64 - antZip& \ +# linux, motif, x86 - antZip& \ +# solaris, motif, sparc - antZip& \ +# solaris, gtk, sparc - antZip& \ +# aix, motif, ppc - antZip& \ +# hpux, motif, PA_RISC - antZip& \ +# macosx, carbon, ppc - antZip + +#Set to true if you want the output to be ready for an update jar (no site.xml generated) +outputUpdateJars = true + +#Set to true for Jnlp generation +#codebase should be a URL that will be used as the root of all relative URLs in the output. +#generateJnlp=false +#jnlp.codebase= +#jnlp.j2se= +#jnlp.locale= +#jnlp.generateOfflineAllowed=true or false generate attribute in the generated features +#jnlp.configs=${configs} #uncomment to filter the content of the generated jnlp files based on the configuration being built + +#Set to true if you want to sign jars +#signJars=false +#sign.alias= +#sign.keystore= +#sign.storepass= + +#Arguments to send to the zip executable +zipargs= + +#Arguments to send to the tar executable +tarargs= + +#Control the creation of a file containing the version included in each configuration - on by default +#generateVersionsLists=false + +############## BUILD NAMING CONTROL ################ +# The directory into which the build elements are fetched and where +# the build takes place. +buildDirectory=. + +# Type of build. Used in naming the build output. Typically this value is +# one of I, N, M, S, ... +buildType=build + +# ID of the build. Used in naming the build output. +# forceContextQualifer = build label +buildId=${forceContextQualifier} + +# Label for the build. Used in naming the build output +buildLabel=${buildId} + +# Timestamp for the build. Used in naming the build output +timestamp=007 + +#The value to be used for the qualifier of a plugin or feature when you want to override the value computed by pde. +#The value will only be applied to plugin or features indicating build.properties, qualifier = context +#forceContextQualifier= + +#Enable / disable the generation of a suffix for the features that use .qualifier. +#The generated suffix is computed according to the content of the feature +#generateFeatureVersionSuffix=true + +############# BASE CONTROL ############# +# Settings for the base Eclipse components and Java class libraries +# against which you are building. +# Base location for anything the build needs to compile against. For example, +# in most RCP app or a plug-in, the baseLocation should be the location of a previously +# installed Eclipse against which the application or plug-in code will be compiled and the RCP delta pack. + +baseLocation=${ECLIPSE_HOME} +#Os/Ws/Arch/nl of the eclipse specified by baseLocation +baseos=linux +basews=gtk +basearch=x86 + +#this property indicates whether you want the set of plug-ins and features to be considered during the build to be limited to the ones reachable from the features / plugins being built +filteredDependencyCheck=false + +#this property indicates whether the resolution should be done in development mode (i.e. ignore multiple bundles with singletons) +resolution.devMode=false + +#pluginPath is a list of locations in which to find plugins and features. This list is separated by the platform file separator (; or :) +#a location is one of: +#- the location of the jar or folder that is the plugin or feature : /path/to/foo.jar or /path/to/foo +#- a directory that contains a /plugins or /features subdirectory +#- the location of a feature.xml, or for 2.1 style plugins, the plugin.xml or fragment.xml +#pluginPath= + +skipBase=true +eclipseURL= +eclipseBuildId= +eclipseBaseURL=${eclipseURL}/eclipse-platform-${eclipseBuildId}-win32.zip + + +############# MAP FILE CONTROL ################ +# This section defines CVS tags to use when fetching the map files from the repository. +# If you want to fetch the map file from repository / location, change the getMapFiles target in the customTargets.xml + +skipMaps=true +mapsRepo=:pserver:anonymous@example.com/path/to/repo +mapsRoot=path/to/maps +mapsCheckoutTag=HEAD + +#tagMaps=true +mapsTagTag=v${buildId} + + +############ REPOSITORY CONTROL ############### +# This section defines properties parameterizing the repositories where plugins, fragments +# bundles and features are being obtained from. + +# The tags to use when fetching elements to build. +# By default thebuilder will use whatever is in the maps. +# This value takes the form of a comma separated list of repository identifier (like used in the map files) and the +# overriding value +# For example fetchTag=CVS=HEAD, SVN=v20050101 +# fetchTag=HEAD +skipFetch=true + + +############# JAVA COMPILER OPTIONS ############## +# The location of the Java jars to compile against. Typically the rt.jar for your JDK/JRE +#bootclasspath=${java.home}/lib/rt.jar + +# specific JRE locations to compile against. These values are used to compile bundles specifying a +# Bundle-RequiredExecutionEnvironment. Uncomment and set values for environments that you support +#CDC-1.0/Foundation-1.0= /path/to/rt.jar +#CDC-1.1/Foundation-1.1= +#OSGi/Minimum-1.0= +#OSGi/Minimum-1.1= +#JRE-1.1= +#J2SE-1.2= +#J2SE-1.3= +#J2SE-1.4= +#J2SE-1.5= +#JavaSE-1.6= +#PersonalJava-1.1= +#PersonalJava-1.2= +#CDC-1.0/PersonalBasis-1.0= +#CDC-1.0/PersonalJava-1.0= +#CDC-1.1/PersonalBasis-1.1= +#CDC-1.1/PersonalJava-1.1= + +# Specify the output format of the compiler log when eclipse jdt is used +logExtension=.log + +# Whether or not to include debug info in the output jars +javacDebugInfo=false + +# Whether or not to fail the build if there are compiler errors +javacFailOnError=true + +# Enable or disable verbose mode of the compiler +javacVerbose=true + +# Extra arguments for the compiler. These are specific to the java compiler being used. +#compilerArg= + +# Default value for the version of the source code. This value is used when compiling plug-ins that do not set the Bundle-RequiredExecutionEnvironment or set javacSource in build.properties +javacSource=1.5 + +# Default value for the version of the byte code targeted. This value is used when compiling plug-ins that do not set the Bundle-RequiredExecutionEnvironment or set javacTarget in build.properties. +javacTarget=1.5 + +################### CUSTOM PROPERTIES ####################################### +# repository location for update site +# comment out - this is passed in from command line +#updateSiteSource=${buildDirectory}/sites/external +# where to place update site build +updateSiteRoot=${user.home}/www/no_crawl/ +updateSiteFolder=${archivePrefix} +updateSiteDestination=${updateSiteRoot}/${updateSiteFolder} diff --git a/eclipse/buildConfig/buildUpdateSite.xml b/eclipse/buildConfig/buildUpdateSite.xml new file mode 100644 index 000000000..1ab7c99a0 --- /dev/null +++ b/eclipse/buildConfig/buildUpdateSite.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/eclipse/buildConfig/customTargets.xml b/eclipse/buildConfig/customTargets.xml new file mode 100644 index 000000000..5a46bfc66 --- /dev/null +++ b/eclipse/buildConfig/customTargets.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse/changes.txt b/eclipse/changes.txt new file mode 100644 index 000000000..93ca3e523 --- /dev/null +++ b/eclipse/changes.txt @@ -0,0 +1,253 @@ +0.9.6: +- Editing default.properties outside of eclipse will automatically update the project +- Fix issue when launching ADT the first time with the SDK Usage panel that could create a deadlock between modal dialogs +- Launched applications from ADT now behave as if they were clicked from the Home screen of the emulator. +- AVD creation dialog now enforce sd card of 9MB or higher +- Fixed issue where add-on with no optional library would not show up as valid targets for application launches. +- Loads the SDK content only when a project requires it. This will make Eclipse use less resources when the SDK contains many versions of Android. +- DDMS plug-in now contains the Allocation Tracker view. +- Lots of fixes in the configuration selector of the Visual Layout Editor. +- Explode mode in the Visual Layout Editor adds a margin to all layout objects so that it's easier to see embedded layouts +- Outline mode in the Visual Layout Editor draws layout outline to make it easier to see layout objects. +- Fixed issues with the New Project Wizard when selecting samples. +- Fixed possible crash when launching applications +- New action in the Logcat view: "Go to problem" lets you go directly from an exception trace output to the code. + +0.9.5: +- misc fixes in the SDK Updater + +0.9.4: +- New "Create project from sample" choice in the New Project Wizard. +- Improvements to the SDK Updater. +- improvements to the AVD Manager (creation and launch dialogs) +- new configuration selector in the Graphical Layout Editor + +0.9.3: +- New wizard to create Android JUnit Test Projects. +- New AVD wizard. +- SDK Updater +- zipalign support + +0.9.1: +- Added an AVD creation wizard to ADT. It is automatically displayed during a launch if no compatible AVDs are found. +- Fixed issue with libs/ folder where files with no extension would prevent the build from finishing. +- Improved error handling during the final steps of the build to mark the project if an unexpected error prevent the build from finishing. +- Fixed issue when launching ADT on a clean install would trigger org.eclipse.swt.SWTError: Not implemented [multiple displays]. + + +0.9.0: +- Projects now store generated Java files (R.java/Manifest.java and output from aidl) in a 'gen' source folder. +- Support for the new Android SDK with support for multiple versions of the Android platform and for vendor supplied add-ons. + * New Project Wizard lets you choose which platform/add-on to target. + * Project properties (right click project in Package Explorer, then "Properties"), lets you edit project target. + * New Launch configuration option to choose debug deployment target. +- Ability to export multiple apk from one project, using resource filters. See the 'android' property for Android projects. +- Support for running JUnit tests on a device/emulator from a new "Android JUnit tests" launch configuration. + +0.8.1: + +- Alternate Layout wizard. In the layout editor, the "create" button is now enabled to easily create alternate versions of the current layout. +- Fixed issue with custom themes/styles in the layout editor. +- Export Wizard: To export an application for release, and sign it with a non debug key. Accessible from the export menu, from the Android Tools contextual menu, or from the overview page of the manifest editor. +- New XML File Wizard: To easily create new XML resources file in the /res directory. +- New checks on launch when attempting to debug on a device. +- Basic support for drag'n'drop in Graphical layout editor. You can add new items by drag'n'drop from the palette. There is no support for moving/resizing yet. +- Undo/redo support in all XML form editors and Graphical layout editor. + +0.8.0: + +- Fixed issue with using custom classes implementing Parcelable in aidl files. Right click the project and choose Android Tools > Create aidl preprocess file for Parcelable Classes. +- Added Custom Themes to theme drop down in the layout editor. +- Customizable debug signing keystore path in preferences +- Customizable HOME package name. + +0.7.1: + +- Layout Editor. + +0.6.1: + +- Fixed install issue when project name contains spaces (requires new emulator image) +- Fixed setup of the New class wizard in the manifest (when clicking on "name" for a class attribute) in the cases where the class and some of its parent packages were missing. +- Properly kill the application that is about to be reinstalled. +- Create missing android folder automatically when building application (caused a signing error) +- Manifest editor: support for uses-library node +- Fixed NPE in editors.xml.descriptors.XmlDescriptors.createPreference +- Fixed assert in MultiEditorPart.setActivePage +- Fixed "connect to debugger" button in DeviceView. Also fixed support for custom process names. + +0.6.0: + +- new launch option for activity. Can choose to launch default activity (finds an activity configured to show up in the home screen), or specific activity, or none. +- normal java resources (non java files placed in package folders) are now properly packaged in the final package, and can be accessed through normal java API such as ClassLoader.getResourceAsStream() +- launch configuration now has an option to wipe emulator data on launch. This always asks for confirmation. +- launch configuration now has an option to disable the boot animation. This will let the emulator start faster on older computers. +- Applications are now signed with a debug key (stored in debug.keystore in ~/.android). +- Installation of application is now more robust and will notify of installation failure. Also installation is blocking, removing issues where ADT tried to launch the activity before the app was installed. +- Tree-based resource editor + content assist in XML editor for layout, menu, preferences, values xml files. Work in progress... + + +0.4.0 (adt 0.4.0, ddms 0.3.0, editors 0.2.0, common 0.1.0) + +- New AndroidManifest editor. +- True multiple device support allowing debugging apps on several device at the same time +- New launch modes for device selection: automatic will launch an emulator if no device are present, automatically target the device if only one exists, and prompt the user if 2+ are connected. Manual mode always prompt the user. +- New classpath container remove the dependencies on the location of android.jar making it easier to share a project through dsvn, cvs, etc... You should fix your project (right click project, choose Android > Fix Project properties) +- Fixed a case where pm would fail and would up end outputting the "usage" text, which would in turn confuse the plugin during parsing. +- Fixed an issue with compiling aidl file when they import project local files. + +0.3.4 (adt 0.3.4, ddms 0.2.3, editors 0.1.0) + +Internal release only. +- Enabled device support. + +0.3.3 (adt 0.3.3, ddms 0.2.3, editors 0.1.0) + +- Support for referenced projects. +- During launch, display if a package conflict occurs when the new application is pushed onto the device. +- You can now change the font of the logcat view. Also indentation is now properly displayed. +- Plugin generated files are now properly marked as derived. This will make Team plugins ignore them. + +0.3.2 + +- XML Highlighting for AndroidManifest.xml (requires WebTools WST plugin) +- Custom java editor for R.java/Manifest.java to make those files non editable. This is to replace the current locking mechanism which causes issues on Mac OS. +- Fixed some issue in the "Restart adb" feature in the device view of ddms. +- Better handling of aidl files and the java files generated from them. +- Plugin now retries to launch the app on the emulator if it fails due to timing issues. +- Skin dropdown in the Emulator/Target tabs is now build from the content of the skin directory, to support developer made skins. +- Emulator control panel. This is a UI on top of the emulator console. it allows you to change the state of the network and gsm connection, and to initiate incoming voice call. + +0.3.1 + +- Fixed issue on winXP/Eclipse 3.2 where errors in the New Project Wizard would not display. +- Added missing intent definition in the AndroidManifest.xml file created by the New Project Wizard. +- Fixed possible NPE in the debug action from the Process View +- Support for Eclipse 3.4 + +0.2.6 / 0.3.0 + +- New Project Wizard now makes it easy to open Android sample code +- Plugin will output a warning if the build id of the device/emulator does not match the sdk build id. +- Java/Debug/ddms perspective now contains direct menus to open some of the ddms views, and to create a new android project. This will require you to reset your perspectives. +- Error during builds now put an error marker on the project instead of displaying an (annoying) dialog box. +- Custom builders now remember their build state when restarting eclipse. +- Properly parse some aapt warnings and don't abort the build when they happen. +- Abort launch and prompt the user if the project contains errors. +- New silent/normal/verbose build output. + +0.2.5 + +- Check compiler compliance level before compilation and abort if different from 1.5 +- Fix Project Properties will fix the project compiler compliance if needed. +- Fixed an issue with multiple source folders. +- Added support for new Manifest.java class (it is automatically generated with R.java if the content of the AndroidManifest.xml requires it) +- Fixed an issue that could result in not packaging code changes. +- Automatic fix of the Launch Configurations when the java package in the manifest is changed. Also improved Launch Config dialog and error alert for erroneous activity names in the Launch Configuration. +- Support for external jars that are not under the project root directory. +- New projects have a default layout. +- Misc fixes for Windows support. + +0.2.4 + +- fixed large resource corruption issue. + +0.2.3 + +- fixed issue related to the integration of dx. +- fixed issue related to the package generation that was modified for windows support. + +0.2.2 + +- Changing the SDK location in the Preferences does not require to restart Eclipse anymore. +- New SDK-Project sync mode in Android preference pane. Default value set to true. If true, all android projects are automatically sync'ed to the SDK defined in the preferences. +- Cases where no emulator is running but a dialog still says "An emulator is running..." should be less frequent. +- Projects do not reference the standard desktop JRE anymore, as android.zip contains the core java library. This will solve the case where using a core class non present on the platform would not generate a compilation error. +- Changing the package defined in the manifest now deletes the R.java class from its previous location. This will require 1 build after upgrading the plugin, before it works. +- Project selection in the Launch Config Dialog now only shows Android projects. +- Launching a debug/run session now checks that the project uses the SDK set in the preferences (This is for the non automatic sync mode). +- Removed obsolete wallpaper mode in the New Project Creation Wizard. +- dx (dalvik code conversion tool) now embedded instead of calling the external version. +- improvements in the parsing of the aapt errors. +- some fixes for windows support. + +0.2.1 + +- fixed bug in logcat search where invalid regexp would cause a crash +- minor improvements to the build/launch process. + +0.2.0 + +- Logcat view. +- File Explorer view. +- Custom options for emulator. In the Launch configuration dialog you can specify custom command line emulator options. See "emulator -help" for available options. +- Android Tools > Export Application Package is now implemented. +- Misc incremental builder fixes. +- Including static .jar files as library in your project will automatically include them in the final APK. Warning: only the .class content is included. + +0.1.10 + +- res and assets folders now fully refresh before the build, ensuring R.java and packaged resources are always up to date. This can be disabled in the preferences under "Android" if this becomes slow due to too many files. + +0.1.9 + +- New Action in the "Processes" view to debug an application that is already running. The source project for this application MUST be opened in the current workspace. +- Building the project now force refreshes the res folder. This should help rebuilding the resources when only binary files were changed from outside eclipse. +- Clean/full builds now compile all aidl files found in the build path (previously only incremental builds would compile them). Also, misc improvements to the incremental builders. +- Starting a run/debug session now asks to save the files and forces a new build to ensure that the latest package is pushed on the device. +- Plugin should be less aggressive when waiting for the emulator to be ready. This should translate in fewer failed launches. + +0.1.8 + +- Fixed Debugger issue introduced in 0.1.6 +- Added Log level preferences for DDMS. Look under Android > DDMS > Advanced. Default error level is Error. + +0.1.7 + +- Fixed issue where java warnings wouldn't trigger a new package. Now only errors stop the packaging like it should be. +- Added more error output in the console during launch. + +0.1.6 + +- New "Android" Console. It receives the error output from external tools such and aidl, dx, and aapt (only when they can't be parsed). Any error force the console to be displayed. +- The Activity Manager on the device/emulator now outputs some messages in the "Android" console when asked to start an activity. This should help you figure out what is wrong if the application doesn't start. +- Fixed a case where the .apk file would be updated with broken code. Now if there are java compile error, the .apk is not touched. +- Added support for manifest with non fully qualified activity java name, yet not starting with a dot. +- Fixed creation of manifest files (through New Project wizard) to use proper namespace for attributes. +- Better error reporting for namespace issue in the manifest. +- "Reset Adb" action from the device view. Use this is the plugin tells you an emulator is running when there are none. +- New "ddms" Console which receives the standard output of ddms. + +0.1.5 + +- Support for new activity declaration inside AndroidManifest.xml +- fixed issue that prevented bin/ to be removed from the buildpath when converting project. + +0.1.4 + +- Changes in the Manifest, now properly trigger a new package of the resources. + +0.1.3 + +- Fixed the "fix project properties" action to remove old framework libraries, just not add new ones. + +0.1.2 + +- aidl builder. The Android Resources PreBuilder now also converts aidl files into java files. +- New Project wizard now allows to make Wallpaper activities instead of gadgets (which are obsolete.) +- Launch shortcuts. Right click in the package explorer allow you to launch the application in debug or run mode directly without creating launch configurations. +- New project wizard and Project conversion now sets up the java doc path for android.zip +- Package builder now supports custom application assets placed in assets/ (which is now created automatically by the New Project Wizard). +- New action: Android Tools > Fix Project Properties, in the package explorer contextual menu. This allows you to fix the framework path (and its javadoc path) in case you change the sdk location. + +0.1.1 + +- Fixed project convertor to add the framework library if missing. + +0.1.0 + +- New project wizard. +- Python script-generated project convertor. +- Incremental builders. +- XML validation for resource files. +- Android Launch Configuration. diff --git a/eclipse/features/com.android.ide.eclipse.adt/.project b/eclipse/features/com.android.ide.eclipse.adt/.project new file mode 100644 index 000000000..beca599f7 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.adt/.project @@ -0,0 +1,17 @@ + + + adt-feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + + org.eclipse.pde.FeatureNature + + diff --git a/eclipse/features/com.android.ide.eclipse.adt/build.properties b/eclipse/features/com.android.ide.eclipse.adt/build.properties new file mode 100644 index 000000000..64f93a9f0 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.adt/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/eclipse/features/com.android.ide.eclipse.adt/feature.xml b/eclipse/features/com.android.ide.eclipse.adt/feature.xml new file mode 100644 index 000000000..f0565a7da --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.adt/feature.xml @@ -0,0 +1,154 @@ + + + + + Android Developer Tools. + + + + Copyright (C) 2007 The Android Open Source Project + + + + Note: kxml2-2.3.0.jar is under the BSD license rather than the EPL. You can find a copy of the BSD License at http://www.opensource.org/licenses/bsd-license.php + Note: groovy-all-1.6.5.jar is under a BSD/Apache license rather than the EPL. For details, please see http://groovy.codehaus.org/faq.html#licence . + + Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and + +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the Program. + +Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse/features/com.android.ide.eclipse.ddms/.project b/eclipse/features/com.android.ide.eclipse.ddms/.project new file mode 100644 index 000000000..f80ff609a --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.ddms/.project @@ -0,0 +1,17 @@ + + + ddms-feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + + org.eclipse.pde.FeatureNature + + diff --git a/eclipse/features/com.android.ide.eclipse.ddms/build.properties b/eclipse/features/com.android.ide.eclipse.ddms/build.properties new file mode 100644 index 000000000..64f93a9f0 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.ddms/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/eclipse/features/com.android.ide.eclipse.ddms/feature.xml b/eclipse/features/com.android.ide.eclipse.ddms/feature.xml new file mode 100644 index 000000000..bf5fe02d7 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.ddms/feature.xml @@ -0,0 +1,245 @@ + + + + + Android Dalvik Debug Monitor Service + + + + Copyright (C) 2007 The Android Open Source Project + + + + Note: jcommon-1.0.12.jar is under the BSD license rather than the APL. You can find a copy of the BSD License at http://www.opensource.org/licenses/bsd-license.php + + jfreechart-1.0.9.jar and jfreechart-1.0.9-swt.jar are under the LGPL rather than the APL. You can find a copy of the LGPL at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt. You can get the source code for these two components at http://android.git.kernel.org/pub/jfreechart-1.0.9.zip + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + + + + + + + + + + + + + + + + + diff --git a/eclipse/features/com.android.ide.eclipse.tests/.project b/eclipse/features/com.android.ide.eclipse.tests/.project new file mode 100644 index 000000000..6a1627622 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.tests/.project @@ -0,0 +1,17 @@ + + + adt-tests-feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + + org.eclipse.pde.FeatureNature + + diff --git a/eclipse/features/com.android.ide.eclipse.tests/build.properties b/eclipse/features/com.android.ide.eclipse.tests/build.properties new file mode 100644 index 000000000..64f93a9f0 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.tests/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/eclipse/features/com.android.ide.eclipse.tests/feature.xml b/eclipse/features/com.android.ide.eclipse.tests/feature.xml new file mode 100644 index 000000000..d9152b60d --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.tests/feature.xml @@ -0,0 +1,30 @@ + + + + + Copyright (C) 2007 The Android Open Source Project + + + + + + + + + + + + + + + + diff --git a/eclipse/plugins/.gitignore b/eclipse/plugins/.gitignore new file mode 100644 index 000000000..72cc8c50b --- /dev/null +++ b/eclipse/plugins/.gitignore @@ -0,0 +1,59 @@ +com.android.ide.eclipse.adt/bin +com.android.ide.eclipse.ddms/bin +com.android.ide.eclipse.tests/bin + +com.android.ide.eclipse.adt/androidprefs.jar +com.android.ide.eclipse.adt/jarutils.jar +com.android.ide.eclipse.adt/kxml2-2.3.0.jar +com.android.ide.eclipse.adt/layoutlib_api.jar +com.android.ide.eclipse.adt/layoutlib_utils.jar +com.android.ide.eclipse.adt/ninepatch.jar +com.android.ide.eclipse.adt/sdklib.jar +com.android.ide.eclipse.adt/sdkstats.jar +com.android.ide.eclipse.adt/sdkuilib.jar +com.android.ide.eclipse.adt/commons-compress-1.0.jar +com.android.ide.eclipse.ddms/icons/add.png +com.android.ide.eclipse.ddms/icons/backward.png +com.android.ide.eclipse.ddms/icons/clear.png +com.android.ide.eclipse.ddms/icons/d.png +com.android.ide.eclipse.ddms/icons/debug-attach.png +com.android.ide.eclipse.ddms/icons/debug-error.png +com.android.ide.eclipse.ddms/icons/debug-wait.png +com.android.ide.eclipse.ddms/icons/delete.png +com.android.ide.eclipse.ddms/icons/device.png +com.android.ide.eclipse.ddms/icons/down.png +com.android.ide.eclipse.ddms/icons/e.png +com.android.ide.eclipse.ddms/icons/edit.png +com.android.ide.eclipse.ddms/icons/empty.png +com.android.ide.eclipse.ddms/icons/emulator.png +com.android.ide.eclipse.ddms/icons/forward.png +com.android.ide.eclipse.ddms/icons/gc.png +com.android.ide.eclipse.ddms/icons/halt.png +com.android.ide.eclipse.ddms/icons/heap.png +com.android.ide.eclipse.ddms/icons/hprof.png +com.android.ide.eclipse.ddms/icons/i.png +com.android.ide.eclipse.ddms/icons/importBug.png +com.android.ide.eclipse.ddms/icons/load.png +com.android.ide.eclipse.ddms/icons/pause.png +com.android.ide.eclipse.ddms/icons/play.png +com.android.ide.eclipse.ddms/icons/pull.png +com.android.ide.eclipse.ddms/icons/push.png +com.android.ide.eclipse.ddms/icons/save.png +com.android.ide.eclipse.ddms/icons/thread.png +com.android.ide.eclipse.ddms/icons/tracing_start.png +com.android.ide.eclipse.ddms/icons/tracing_stop.png +com.android.ide.eclipse.ddms/icons/up.png +com.android.ide.eclipse.ddms/icons/v.png +com.android.ide.eclipse.ddms/icons/w.png +com.android.ide.eclipse.ddms/icons/warning.png +com.android.ide.eclipse.ddms/libs/jcommon-1.0.12.jar +com.android.ide.eclipse.ddms/libs/jfreechart-1.0.9-swt.jar +com.android.ide.eclipse.ddms/libs/jfreechart-1.0.9.jar +com.android.ide.eclipse.ddms/src/com/android/ddmlib +com.android.ide.eclipse.ddms/src/com/android/ddmuilib +com.android.ide.eclipse.tests/kxml2-2.3.0.jar +com.android.ide.eclipse.tests/unittests/com/android/ddmlib +com.android.ide.eclipse.tests/unittests/com/android/sdklib +com.android.ide.eclipse.tests/unittests/com/android/sdkuilib +com.android.ide.eclipse.tests/unittests/com/android/layoutlib + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.classpath b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath new file mode 100644 index 000000000..23d84a733 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.gitignore b/eclipse/plugins/com.android.ide.eclipse.adt/.gitignore new file mode 100644 index 000000000..d392f0e82 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/.gitignore @@ -0,0 +1 @@ +*.jar diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.project b/eclipse/plugins/com.android.ide.eclipse.adt/.project new file mode 100644 index 000000000..c7b1ad476 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/.project @@ -0,0 +1,30 @@ + + + adt + + + SdkLib + SdkUiLib + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF new file mode 100644 index 000000000..20a3d64c9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF @@ -0,0 +1,131 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Android Development Toolkit +Bundle-SymbolicName: com.android.ide.eclipse.adt;singleton:=true +Bundle-Version: 0.9.6.qualifier +Bundle-ClassPath: ., + jarutils.jar, + androidprefs.jar, + sdkstats.jar, + kxml2-2.3.0.jar, + layoutlib_api.jar, + ninepatch.jar, + layoutlib_utils.jar, + sdklib.jar, + sdkuilib.jar, + commons-compress-1.0.jar, + groovy-all-1.6.5.jar +Bundle-Activator: com.android.ide.eclipse.adt.AdtPlugin +Bundle-Vendor: The Android Open Source Project +Require-Bundle: com.android.ide.eclipse.ddms, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.debug.core, + org.eclipse.debug.ui, + org.eclipse.jdt, + org.eclipse.ant.core, + org.eclipse.jdt.core, + org.eclipse.jdt.ui, + org.eclipse.jdt.launching, + org.eclipse.jface.text, + org.eclipse.ui.editors, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui.console, + org.eclipse.core.filesystem, + org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.ui.forms, + org.eclipse.gef, + org.eclipse.ui.browser, + org.eclipse.ui.views, + org.eclipse.wst.sse.core, + org.eclipse.wst.sse.ui, + org.eclipse.wst.xml.core, + org.eclipse.wst.xml.ui, + org.eclipse.jdt.junit, + org.eclipse.jdt.junit.runtime, + org.eclipse.ltk.core.refactoring, + org.eclipse.ltk.ui.refactoring, + org.eclipse.core.expressions +Eclipse-LazyStart: true +Export-Package: com.android.ide.eclipse.adt;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.actions;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.build;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.layout;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.layout.configuration;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.layout.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.layout.gle1;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.layout.parts;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.layout.uimodel;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.manifest;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.manifest.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.manifest.model;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.manifest.pages;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.menu;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.menu.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.resources;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.resources.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.resources.uimodel;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.ui;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.ui.tree;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.uimodel;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.xml;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.editors.xml.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.launch;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.launch.junit;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.launch.junit.runtime;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.preferences;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.project;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.properties;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.refactorings.extractstring;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.resources;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.resources.configurations;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.resources.manager;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.resources.manager.files;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.sdk;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.ui;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.wizards.actions;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.wizards.export;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.wizards.newproject;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.wizards.newxmlfile;x-friends:="com.android.ide.eclipse.tests", + com.android.jarutils;x-friends:="com.android.ide.eclipse.tests", + com.android.layoutlib.api;x-friends:="com.android.ide.eclipse.tests", + com.android.layoutlib.utils;x-friends:="com.android.ide.eclipse.tests", + com.android.ninepatch;x-friends:="com.android.ide.eclipse.tests", + com.android.prefs;x-friends:="com.android.ide.eclipse.tests", + com.android.sdklib;x-friends:="com.android.ide.eclipse.tests", + com.android.sdklib.internal.avd;x-friends:="com.android.ide.eclipse.tests", + com.android.sdklib.internal.project;x-friends:="com.android.ide.eclipse.tests", + com.android.sdklib.internal.repository;x-friends:="com.android.ide.eclipse.tests", + com.android.sdklib.repository;x-friends:="com.android.ide.eclipse.tests", + com.android.sdklib.xml;x-friends:="com.android.ide.eclipse.tests", + com.android.sdkstats;x-friends:="com.android.ide.eclipse.tests", + com.android.sdkuilib.internal.repository;x-friends:="com.android.ide.eclipse.tests", + com.android.sdkuilib.internal.repository.icons;x-friends:="com.android.ide.eclipse.tests", + com.android.sdkuilib.internal.tasks;x-friends:="com.android.ide.eclipse.tests", + com.android.sdkuilib.internal.widgets;x-friends:="com.android.ide.eclipse.tests", + com.android.sdkuilib.repository;x-friends:="com.android.ide.eclipse.tests", + com.android.sdkuilib.ui;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.archivers;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.archivers.ar;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.archivers.cpio;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.archivers.jar;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.archivers.tar;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.archivers.zip;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.changes;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.compressors;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.compressors.bzip2;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.compressors.gzip;x-friends:="com.android.ide.eclipse.tests", + org.apache.commons.compress.utils;x-friends:="com.android.ide.eclipse.tests", + org.kxml2.io;x-friends:="com.android.ide.eclipse.tests", + org.kxml2.kdom;x-friends:="com.android.ide.eclipse.tests", + org.kxml2.wap;x-friends:="com.android.ide.eclipse.tests", + org.kxml2.wap.syncml;x-friends:="com.android.ide.eclipse.tests", + org.kxml2.wap.wml;x-friends:="com.android.ide.eclipse.tests", + org.kxml2.wap.wv;x-friends:="com.android.ide.eclipse.tests", + org.xmlpull.v1;x-friends:="com.android.ide.eclipse.tests" + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL b/eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL new file mode 100644 index 000000000..e69de29bb diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE b/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE new file mode 100644 index 000000000..49c101df7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE @@ -0,0 +1,224 @@ +*Eclipse Public License - v 1.0* + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE +PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF +THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +*1. DEFINITIONS* + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and +documentation distributed under this Agreement, and +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and +are distributed by that particular Contributor. A Contribution +'originates' from a Contributor if it was added to the Program by such +Contributor itself or anyone acting on such Contributor's behalf. +Contributions do not include additions to the Program which: (i) are +separate modules of software distributed in conjunction with the Program +under their own license agreement, and (ii) are not derivative works of +the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +*2. GRANT OF RIGHTS* + +a) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free copyright +license to reproduce, prepare derivative works of, publicly display, +publicly perform, distribute and sublicense the Contribution of such +Contributor, if any, and such derivative works, in source code and +object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free patent license +under Licensed Patents to make, use, sell, offer to sell, import and +otherwise transfer the Contribution of such Contributor, if any, in +source code and object code form. This patent license shall apply to the +combination of the Contribution and the Program if, at the time the +Contribution is added by the Contributor, such addition of the +Contribution causes such combination to be covered by the Licensed +Patents. The patent license shall not apply to any other combinations +which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the +licenses to its Contributions set forth herein, no assurances are +provided by any Contributor that the Program does not infringe the +patent or other intellectual property rights of any other entity. Each +Contributor disclaims any liability to Recipient for claims brought by +any other entity based on infringement of intellectual property rights +or otherwise. As a condition to exercising the rights and licenses +granted hereunder, each Recipient hereby assumes sole responsibility to +secure any other intellectual property rights needed, if any. For +example, if a third party patent license is required to allow Recipient +to distribute the Program, it is Recipient's responsibility to acquire +that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright +license set forth in this Agreement. + +*3. REQUIREMENTS* + +A Contributor may choose to distribute the Program in object code form +under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties +and conditions, express and implied, including warranties or conditions +of title and non-infringement, and implied warranties or conditions of +merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and +consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are +offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such +Contributor, and informs licensees how to obtain it in a reasonable +manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained +within the Program. + +Each Contributor must identify itself as the originator of its +Contribution, if any, in a manner that reasonably allows subsequent +Recipients to identify the originator of the Contribution. + +*4. COMMERCIAL DISTRIBUTION* + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, the +Contributor who includes the Program in a commercial product offering +should do so in a manner which does not create potential liability for +other Contributors. Therefore, if a Contributor includes the Program in +a commercial product offering, such Contributor ("Commercial +Contributor") hereby agrees to defend and indemnify every other +Contributor ("Indemnified Contributor") against any losses, damages and +costs (collectively "Losses") arising from claims, lawsuits and other +legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the +Program in a commercial product offering. The obligations in this +section do not apply to any claims or Losses relating to any actual or +alleged intellectual property infringement. In order to qualify, an +Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial +Contributor to control, and cooperate with the Commercial Contributor +in, the defense and any related settlement negotiations. The Indemnified +Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those +performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor +must pay those damages. + +*5. NO WARRANTY* + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED +ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES +OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR +A PARTICULAR PURPOSE. Each Recipient is solely responsible for +determining the appropriateness of using and distributing the Program +and assumes all risks associated with its exercise of rights under this +Agreement , including but not limited to the risks and costs of program +errors, compliance with applicable laws, damage to or loss of data, +programs or equipment, and unavailability or interruption of operations. + +*6. DISCLAIMER OF LIABILITY* + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR +ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR +DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED +HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +*7. GENERAL* + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further action +by the parties hereto, such provision shall be reformed to the minimum +extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including +a cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or +hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails +to comply with any of the material terms or conditions of this Agreement +and does not cure such failure in a reasonable period of time after +becoming aware of such noncompliance. If all Recipient's rights under +this Agreement terminate, Recipient agrees to cease use and distribution +of the Program as soon as reasonably practicable. However, Recipient's +obligations under this Agreement and any licenses granted by Recipient +relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and may +only be modified in the following manner. The Agreement Steward reserves +the right to publish new versions (including revisions) of this +Agreement from time to time. No one other than the Agreement Steward has +the right to modify this Agreement. The Eclipse Foundation is the +initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is +published, Contributor may elect to distribute the Program (including +its Contributions) under the new version. Except as expressly stated in +Sections 2(a) and 2(b) above, Recipient receives no rights or licenses +to the intellectual property of any Contributor under this Agreement, +whether expressly, by implication, estoppel or otherwise. All rights in +the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to +this Agreement will bring a legal action under this Agreement more than +one year after the cause of action arose. Each party waives its rights +to a jury trial in any resulting litigation. + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/about.ini b/eclipse/plugins/com.android.ide.eclipse.adt/about.ini new file mode 100644 index 000000000..73bf13901 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/about.ini @@ -0,0 +1 @@ +featureImage=icons/android_32x32.png \ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/build.properties b/eclipse/plugins/com.android.ide.eclipse.adt/build.properties new file mode 100644 index 000000000..f0cfc342c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/build.properties @@ -0,0 +1,20 @@ +bin.includes = plugin.xml,\ + META-INF/,\ + icons/,\ + .,\ + templates/,\ + about.ini,\ + jarutils.jar,\ + androidprefs.jar,\ + sdkstats.jar,\ + kxml2-2.3.0.jar,\ + layoutlib_api.jar,\ + layoutlib_utils.jar,\ + ninepatch.jar,\ + sdklib.jar,\ + sdkuilib.jar,\ + commons-compress-1.0.jar,\ + groovy-all-1.6.5.jar,\ + gscripts/ +source.. = src/ +output.. = bin/ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy new file mode 100755 index 000000000..3a7cfe7e1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.adt.gscripts; + +import com.android.ide.eclipse.adt.editors.layout.gscripts.BaseViewRule; +import com.android.ide.eclipse.adt.editors.layout.gscripts.INodeProxy; +import com.android.ide.eclipse.adt.editors.layout.gscripts.DropZone; +import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect; +import com.android.ide.eclipse.adt.editors.layout.gscripts.Point; + +import java.util.Map; +import java.util.ArrayList; + + +/** + * An {@link IViewRule} for android.view.View and all its derived classes. + * This is the "root" rule, that is used whenever there is not more specific rule to apply. + */ +public class AndroidViewViewRule extends BaseViewRule { + + // TODO if there's nothing to implement here, I might as well remove it. + // Before that, make sure the engine can deal with the lack of a base class + // fallback when navigating the hierarchy. + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.AbsoluteLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.AbsoluteLayout.groovy new file mode 100755 index 000000000..179c37207 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.AbsoluteLayout.groovy @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.adt.gscripts; + +import com.android.ide.eclipse.adt.editors.layout.gscripts.BaseViewRule; +import com.android.ide.eclipse.adt.editors.layout.gscripts.INodeProxy; +import com.android.ide.eclipse.adt.editors.layout.gscripts.DropZone; +import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect; +import com.android.ide.eclipse.adt.editors.layout.gscripts.Point; + +import java.util.Map; +import java.util.ArrayList; + +/** + * An {@link IViewRule} for android.widget.AbsoluteLayout and all its derived classes. + */ +public class AndroidWidgetAbsoluteLayourRule extends BaseViewRule { + + // ==== Drag'n'drop support ==== + + /** + * Called when a drop operation starts, whilst the d'n'd is dragging the cursor over the + * views. The purpose of the drop operation will be to create a new element. + *

+ * For the AbsoluteLayout, the whole bounds of the view is a suitable drop zone. + */ + public ArrayList dropStart(INodeProxy targetNode) { + DropZone d = new DropZone(targetNode.getBounds(), null /*data*/ ); + return [ d ]; + } + + /** + * Called after the user selects to drop the given source into one of the drop zones. + */ + public void dropFinish( + String sourceFqcn, + INodeProxy targetNode, + DropZone selectedZone, + Point where) { + int x = where.x - targetNode.getBounds().x; + int y = where.y - targetNode.getBounds().y; + + debugPrintf("AbsL.drop: add ${sourceFqcn} at coord ${x}x${y}"); + + INodeProxy e = targetNode.createChild(sourceFqcn); + e.setAttribute("layout_x", "${x}dip"); + e.setAttribute("layout_y", "${y}dip"); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy new file mode 100755 index 000000000..2a0bc3beb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.adt.gscripts; + +import com.android.ide.eclipse.adt.editors.layout.gscripts.BaseViewRule; +import com.android.ide.eclipse.adt.editors.layout.gscripts.INodeProxy; +import com.android.ide.eclipse.adt.editors.layout.gscripts.DropZone; +import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect; +import com.android.ide.eclipse.adt.editors.layout.gscripts.Point; + +/** + * An {@link IViewRule} for android.widget.LinearLayout and all its derived classes. + */ +public class AndroidWidgetLinearLayourRule extends BaseViewRule { + + // ==== Drag'n'drop support ==== + + /** + * Called when a drop operation starts, whilst the d'n'd is dragging the cursor over the + * views. The purpose of the drop operation will be to create a new element. + *

+ * Drop targets that can't accept child views should always return null. + *

+ * Drop targets that can accept child views must return a non-empty list of drop zones, + * customized to the actual bounds of the target. + * The drop zones will be visually shown to the user. Once the user releases the mouse + * in one of the drop zone, the dropAccept/dropFinish methods will be called. + *

+ * Note that at this stage, the drop operation does not offer a way to know what is going + * to be dropped. We just know it's a view descriptor, typically from the layout palette, + * but we don't know which view class yet. + * + * @param targetNode The XML view that is currently the target of the drop. + * @return Null or an empty list if the rule rejects the drop, or a list of usable drop zones. + */ + public ArrayList dropStart(INodeProxy targetNode) { + + // for testing, we're going to make 2 drop zones: top and bottom. + // TODO find inner elements bounds & orientation, add margings + def r = targetNode.getBounds(); + DropZone d1 = new DropZone(); + DropZone d2 = new DropZone(); + r.h /= 2; + d1.bounds.set(r); + d2.bounds.set(r); + d2.bounds.y += r.h; + + return [ d1, d2 ]; + } + + /** + * Called after the user selects to drop the given source into one of the drop zones. + *

+ * This method should use the methods from the {@link INodeProxy} to actually create the + * new XML matching the source descriptor. + * + * @param sourceFqcn The FQCN of the view being dropped. + * @param targetNode The XML view that is currently the target of the drop. + * @param selectedZone One of the drop zones returned by {@link #dropStart(INodeProxy)}. + */ + public void dropFinish( + String sourceFqcn, + INodeProxy targetNode, + DropZone selectedZone, + Point where) { + // TODO + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.ListView.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.ListView.groovy new file mode 100755 index 000000000..7ac0292ba --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.ListView.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.adt.gscripts; + +import com.android.ide.eclipse.adt.editors.layout.gscripts.BaseViewRule; +import com.android.ide.eclipse.adt.editors.layout.gscripts.INodeProxy; +import com.android.ide.eclipse.adt.editors.layout.gscripts.DropZone; +import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect; +import com.android.ide.eclipse.adt.editors.layout.gscripts.Point; + +import java.util.Map; +import java.util.ArrayList; + + +/** + * An {@link IViewRule} for android.widget.ListView and all its derived classes. + * This is the "root" rule, that is used whenever there is not more specific rule to apply. + */ +public class AndroidWidgetListViewRule extends BaseViewRule { + + // ==== XML Creation ==== + + /** + * The default for new views is to be wrap_content on both width/height. + * However ListView is special in that ideally we want fill_parent width by default. + */ + public Map getDefaultAttributes() { + // TODO: find a way to plug in the new value match_parent. + return [ "layout_width" : "fill_parent" ]; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png new file mode 100644 index 0000000000000000000000000000000000000000..eefc2ca300a00e1d80a61c8ddf21416d8e4215db GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k)AG&Ar`&K2@)9xI)b0gxKJ;$ z@2d5hteGFA)*s?ISiw-o+`%9seSj%_;crRV2OPp;B91ZtT*dc)7C*OSk@y84w~dmq tGOHP1eDs-G;=r}1{rLaip4n(#ox@Z6T@rSdZUT*bX)+;4MOd16 ze&6nSk>qnE>zL~C7GFlQ*$GK6+6)A02pCQl$aH%ZT`hL}QFWqey^66HScxi1Nk62PF8_PioLaA(&wNQjO6xRID0Xd?2Nj z25k#%fwhlqZMWUF`@cPGIwY0+;i@E-8+h581#QO#jL=w&Q0mY zE(hDd(QC}P3akL1ry8(pQ|HybnuW6S;2}_Xjd72G;!G#CWTC(mI09YpKwiUF4mP{sA6E#ex zCE6R{DH#Wcfj`B3Ecj_5M>C;?W9@v~pc)Bd$KwHF%D(cil^v6IhC6g@|HObNw z=Sftc3_J&R%3#_LCBDSflWU~~m%%;YZ7>U70KKUc;9Nc+`zz(ZUEsD%r0LNRT&D_n zorjsFrs9)mQW}%OI9rp;&n{CD6K2SOM$DG?;%aGkwtZi|6?FlM$(9VdM#_L&? zVm%t9TRl&TzSCAt-sw{`IQ|8xid8%__y;8suM~|&NYiZM9>H3-xF4Y*-wB!u{7IT> z%YHB5iFG4P^PzECSO1EbcwnrRB6^Tq1$9(iwpCb#SH~+JnLG);rN)W}DXh=qSw5){ ztfa}qG=<0@i{_wwn_C_BwUd8gh`fJy(!^{pRh4d_4UMk}(+7fMG_x>5o|28QiU#Et zsm65|xr^5dOZo9kRV>->hM~wDt*PDZSY5T-&6coWr3H>EdURbI%xR||`wvioy^>aX z9;e?gAE8nBo0VGMuwTRgTdUS}H}!ds(V5|Q$!00c)&)_#RG`3&rZF1s#j5gL@Cr3o z@1>Hwo2d|rag4EInkb&JkYk}%O99WNWNHEJY6q(rne2$phkQO{KrLG#9`TjMYpBV+ zgSrL|QA7Cy906k07bd=<(dl2s>e=rcAHLahm@7|BsE#RPfOk4?OaXgj+<*4_?&0^s zf#4`r7PU}$VKYqq3(Wr}eD6he>=bpOEWZiv@X#NlpV9fzPpQ~hM|MjQox5@rG1D`J z4EV+*Peq2PQnJwK?2A#4VzxDc8YMJJy7Y=JoOY8SJJXoKjt zh9Z8X=R(pAHG>0*aS%#ufq?DE=Qgp?#9A0U1MUTFrWe8(^DNZ6AA7-_7S-Orw*Dom zS+-5=4Z-kqPN(8i^7`<<@|5TVxM<$pl?2^!pQh;;#k`&AzDMRx9NAgREL;W}y&{PLa_1#)g0%QQE zDB`ElOuiY-Vy&c!*Qmj0ER)ZJkG>F|_Zl@APUJVAN5D20L3rsxGy+|RiTWrZ?aLT3 z+xDL(rc1=mqzkW6WuGIgu;B9>D2LED@Q)_C{Dbm;r4WrE8zFH>dLQUVDf%SixHcz} wLT@tYb>+woat2s#pnY|Ywx)i$Q1d7F0qOZ;^Tt;Vi2wiq07*qoM6N<$g7rem?f?J) literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_file.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_file.png new file mode 100644 index 0000000000000000000000000000000000000000..be9b8e6b413c32440cac1881a72036b91448ea45 GIT binary patch literal 519 zcmV+i0{H!jP)Xd81vw4knX7 zefnFIO5pMshDoTrr%skW%qU2HKEC;Z%q1|*ri7pbsUJX29mwIex4R1BpI58XVQ;C$ z-09>!1Qg7j50&|FJI$3~EYUh)%*^vJ|4cK6L^M{yz@Mzx2m!|!lik-r*OtISie*mz z3eT|)>0=2iro$LwjR>%^xE1D|tPVq#fW!4t2Zr<#D@GuKE)f`7%L|(-8Usa8LmKhw z-Zz3FVH_Q{-hmw6FKi&FLoW2s!qf%F207U8tV9#Y?WKnj(5ZzI7miX(b~lP`n=NO2D&7l^F_ zQfX*00x6_OwOgD*`Xlqc^;>J~?AYtw9q%OZlUBR-&OXn4@9%rN>qw!cO zr~!yXBA%3f{#`suyBXR}e|T)M?%3qtL_pHb)3ZlmCr&c>?6L^xYoz4s3fGye7}%OVapx1~uVlE~<6v1_0K> z0>;^e-{Xb15&$vFpdgnd2!N~(0D3eh*@Co)b|fYNRboA2)8qiQ{zcm58rbRKy14M;H%vZq8ZAH zO)Rde!1aYVO4vtIhj;nx$YQ~?#n3$A4HJ@A&eA;7zRDmeb*k|eOC#lZIzR6>6Z;jK zM}!yeM8?u|e%w3Gw(@x#|(m zS3Tm^$``(L|M=(hMsFkC0HEI#0E&-p*M)&CYwIhX^sPR~S@Dnk>cb`LJ0wLr_5nBm zFeV2)epSM4<$(U0 zWssokglJ3rwT~^3;2E%PdA&YK!lj4yopJ;Xf-tk?+n+C(R|boZmTLe!d0MVJ2N$*~ z!@W7NKwvqSzax$S7|R3B#21t_^W>YKZj5bmTPz-yM1{bDZTaS>3w7?UDu48w_05oo znBxJh( zt#9N3VE~YC=Nzmk_X5?dzf?QMg8I15qg3J?c!Eu50g#H?%DT(s9S#gL)8wW|!4m*y zM;0^J7vfokg;S2rkVI4B$Dq8j9HpwUfA_GGC6Y`j z0LbIU=$VT55Wvd_ldL?-NKcW{tWz`O)Uh?v;VO+?@{~NA7%NN0!qemlo@(@0$cdl67n`X)Sxi|!7}{{ovvx&U+m=mM}cgpyozTLVB9 ztI!Pr!vJXY`f9wux#o-j%~GZyVH}VExO?4K53CaX~Wyg w@gKas3S-tC-;gvx<>WSJnSE7}a#qF6+qXI~D)C2O16so1>FVdQ&MBb@0MGF@H~;_u literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/androidjunit.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/androidjunit.png new file mode 100644 index 0000000000000000000000000000000000000000..25826dce773d24bec4b87f86e814bdefef638505 GIT binary patch literal 393 zcmV;40e1e0P)(bu;eV6wl%QIx`oy#!OC4~{B895d}hT;SoORN4fvJW{^t=8i4FvY;3Rr4r|-~R~}vtcPci8xWt%1QFq|h zDTemT``~JBynFy=yU6RplP+8mjlq0pn4*FPD(XOkD>*u9SLsJ5b5 z+J2prnF*;8P9IiGY;3E1>oALd(S4oE=h%QL70(Y)*K0-AbW|YuPgfvE&*{_DdX~Jf1uE7PZ!4!i_>=} zZ1iGw6lmLjIaL-fS}n_o`_Y3BU0^ccP!m#;_W;n}=3Un^MWTdP2t& z)kS8TH|V&nzbZd%W_|hpne}Y{Y(CA<$j$58RHb<;Ye!XVNiA2(L<{zN4ao`Ib1Kry z_lrun{G8b_(c<0X2MVY3RqrwNe$XrWvGaQ4rDq%#>}CfapiuojT6m<^$l;-d+oAUdo1}+eILW^Th1p#s-D~cdYi%1)z4*}Q$iB} DJM@i* literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/clipping.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/clipping.png new file mode 100644 index 0000000000000000000000000000000000000000..6f2a35f6a8a250190d6e5c15e1601060ae308205 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`d7dtgAr^vnCpq#p7;vzxe*f&m ze$xr-5_u)M&8wNjmN{4-JHGksuf-2$@V(8eKFKN|n6zTm&kLU2=j7&2H?#Wmg|Fe= zONNX!d=K)K6BJFtBaFWYeCnTiH2K`F*wP6THMW25k-U=Z#`gE59;5Czopr E0D=D>LI3~& literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png new file mode 100644 index 0000000000000000000000000000000000000000..10057c88a5caab110f1df4f933ccae4067c581c9 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hU0^K@|xskoK&=l_3uW)1@*Bc_@EB_%yr7#NSRT<-CCEOeNSfvJOY$%5UxrOz;4 zY%y8D+`y1@gR-^W*IQ_uL0}FfuZ8G$$r7aBGR%J}Eq@nAE}=thg<}T`c)dk503y?3?>7)~_VY z7cXa*;pCjHIAa!P!{6Bw65sx|C)6Z5Ul7=2dYO}f;bpAM${pXc*n##jc)I$ztaD0e F0sw{HH1Yrd literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..fae5e96d71abb23fcf707afc5999012ac93fa332 GIT binary patch literal 302 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hTL@pN$vskoK&=l_3uW@Y8a^=w8A0s6`g42Dh_o4z#bq%m+PsKp2!VQOGVQaT=% z!XV(F^Fw9gPlNnL)#3^bGbV^Ot6UU4a3rB^VKkF~gAb1gQ01Z<3_#_MWm?-Vw0A!2 z$^vn|>rXqw26&@MZiKc_z}qLBdOd~Y|N=LIj8QJM)!_4@qZ>DVHcHo6WIcK2$+?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hU$^mK6yskoK&=l_3u=A8`_Cw{bdU@#P1EihX`z<`NGK-h4WBiBUU#?HnY42(xQ z&U2sOni$)p!zSRMv*d?lq%sc!i-1SNp8yu728N`@Sw49>20#&EBX;>-ZytpP2}Qmi z{*%@kd{r=WV3_!ZX+m0xAyAt literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png new file mode 100644 index 0000000000000000000000000000000000000000..a727dd53df01aafc1c167f557ed15a001f9f00d6 GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hTr^>lFzskoK&=l_3uW>p4-hKK|nH#awt$qj7ANy6^o%mNNQM|Lr!2%HH>VGwZ8 z`H^#Q8KW!%i-4oJfRT(s14kR(TfWd*mFlZJZ z15-z5qZ3fGh`LGxLz1GK@HU3Fm5dw;U3)=-(ja4xEU~J22;#XbJ2Wsd@K5rQj;~fv Q13Hkw)78&qol`;+08E%lGXMYp literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png new file mode 100644 index 0000000000000000000000000000000000000000..2540fbbc07cdc6d56646cbccdf9f6430d114aa2c GIT binary patch literal 308 zcmV-40n7f0P)QweVq*_K z!>hD!c0WC7qybJ*l2wQx-VpGW2<)1z{U%o#ABM>p?oi~}gaLkX0?B`EDlq?JHCsI~UBDeDAhbtPCPaXs{3O4v*Qu(;bP0l+XkK|JN!W literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc95d7cf30060ab8cfd0d0391e4d9b06d4a2071 GIT binary patch literal 463 zcmV;=0WkiFP)=! zhXeV1K6*Z%7Gbqoq1WpX0GiEal*wewDK?vp9*@Tn4u_FUCXq^|*sj-W?p~|akVqsD zi9}S)TVcE1t~uRqw_^b*1_A*r77MdzB=h;aMHGy(O7wEM=!Q>Zh#(e=F|b~*LtTjr z#xbAIp+pVW+Kom7iX#}d{eB;nN(C~yN~Y6kc)ecU#bOb;Tn_Da+tI)wvfuBaMxIWm ztX?XWxU0Iq@vzR04yG+o;8v@};N@}&P1DfnbS&~@GC?#NMYGxD+?S2ZuHWyc(P+ee zFc_qAxlH5n*n+9AHNKDvg@ToHIK$ykKOT?f>pCj#!8f@X#9OgCTlW9}002ovPDHLk FV1lpN+!_D? literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png new file mode 100644 index 0000000000000000000000000000000000000000..aefffe4a77f83e88dc41306ae7559a22ac4b6b4a GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hVJd%8G=RNPAX^Z&m+vnqo^Lqr0Po15DtAEu}?9E%v4Iyz;}o~UAA;z&7FpCY_b z^N>2bfP)W@2vY+?(!n$>z37dM4GfEJure^ZY-D6$Jkq3_w;UHx3vIVCg!0E%cuZ~y=R literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png new file mode 100644 index 0000000000000000000000000000000000000000..c2bb79aad2ba10fcdd8fc9077747a6af8e93d074 GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hU$@^op4-h8YqXEDVe;8yOiGTdp&>xw$p{W9Cp$bDG5|$t2+5 zbL2+Pd4UFoB$qzc2~G+LN1khyR06q&(oSzmJ^iowhXK!{$3?sj42BM|ALQFE{@iiI zu||LmXut|ZjdM}EN;H}6>^4<3Ff6j*VPNVIj1D>`_G$+YP{Epu9qT0KuV!H4NOx+= sJhWVr1t>r9(j5)~2a$BO1_ow^0_CKg60O&s0A0=C>FVdQ&MBb@0CLD)jQ{`u literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png new file mode 100644 index 0000000000000000000000000000000000000000..0f0e883f049288138260436896d2500338af21e2 GIT binary patch literal 664 zcmV;J0%!e+P)z@;j(q!3lK=n!AY({UO#lFTB>(_`g8%^e{{R4h=>PzA zFaQARU;qF*m;eA5Z<1fdMgRZ;4oO5oRCwBA{Qv(y0}z0Tv>qJ>bTF&r3W#Ea@psQq z|L>Y{6V3++Acp@?klL;N9~~fxfz&bVny&W$-{1HDK{QMZAb^-)HY3@@h-@PaY@4L= z|M%~|U^a-}HdzJiYJdP@L05QbT@_eDFQ^SJw!T~8zkk*Sh>O2HVDLy>1GWMr4iG>n z23%ZQ^-^OCK;UYnBEZ9G=?AuxfTWIMgERo-zUi5ECq*K;FQn83ZcwYyPX+ ztz!83{SVj$KY+A-(iyNEKmal1X4azw8VsWZ;>Sb(|NnRh#xqNV|37~A@&Dur3%~&h z5J2FNj816zj~Be&{Qv*gn{Y!~JD0#?0U&_DVesV#8w10$r3`oh`%eajzdymuAHNVm z2M|DD*0-NP>enZ*fN%MChMzxwF>tW4!_pTd^ci+DFakp!n>#2ntCx`vw31{`tvJP#pqM0rDJJBa)}VCjMub+_{C})`0}LlK=vU y5tmm;T00005PiEFB9V|l1JZ-2h$wjNDQZLy1y5o@D@gRvlPEp*U{69R-YZ&)M=e&2Q0lEn zE7;bPp2UM7h!%~Kv0=*Z|D78;6IU{eI%<#0@a7J^(n2% zJ*(xhm)2~&K;+p+_`#dke$>>Lmb!ztwOYd#?# zOl%pNfN)KqHmV5(EDO0e3DiA(eN!*~L@fChp!c8K(^3;~=;%mqGhf7%65-Kh953{H zwT7`NoGi}3B(rtnpFppyKn<5&sXewMA*1z(49=l&aRywT;19*v&>VuXM%_ZGY@zhe z21;HC7qv}sJRD!c>FOk$zE=pfcHOJeCUOjx0mWlzpHa{y{)WVt;qOR%=pZqvJ!(i$P4O7o211yK zly5wI9sGvgl}_eCojp)r@feywiM(YpX|Goxuc_V?^N`J^y|apn$It|7!VzS4H@SCj dw5h6ijK8hN%PmH3`TGC>002ovPDHLkV1ggU{RaR5 literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png new file mode 100644 index 0000000000000000000000000000000000000000..423c3cd19c770062fd0573ccf7864f6bee3b2f67 GIT binary patch literal 325 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hVh_H=O!skoK&=l_3u=8X*#Ckn3kW3RkP!hyk1aOFhtn*u484Gc*tPnb%04H$|! z9T+wWT(T*D7h_>>X({QW+|9@$a6)B`L<(oX!+NJA2F4>t9GDnx&Y3c$MW_SF^YGG1 zUmdK II;Vst0DcW&)&Kwi literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png new file mode 100644 index 0000000000000000000000000000000000000000..9608cd67d81fd087058f66d09c4e044c5b646cc3 GIT binary patch literal 445 zcmV;u0Yd(XP)L_3YCZuLP2$-Lvh~DTzBK< z$>Y6EHZwbW=A4i7Fvf}d7B nArDu_JWjXUwRgMS+rRJ|tr5L(`P@gX00000NkvXXu0mjfC7sUP literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png new file mode 100644 index 0000000000000000000000000000000000000000..b4ddc874a4582f77538437598cfc2d3a375a0d57 GIT binary patch literal 321 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hU0_jGX#skoK&=l_3uW@Y8a^%`dn7(BFRjte+&f+3iDi6kRaM`xoGgMdSigq{ME z!fpnIhKLKQ9;^(EEe~1C*xU?`m1rxoEM{zAI3&fkUm~$e;^DnRPc0i5k`67E%x*{& zWady1%VBis=;dKx>fpS*+T+}@gKlgLj4p*%3>*qwdx1uaR5L0x%=oagK?0<&h4auP zc)I$ztaD0e F0sv0pU)%ry literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png new file mode 100644 index 0000000000000000000000000000000000000000..65365765a8f538351d582504588f8db22ec86ca4 GIT binary patch literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>?0v z(btiIVPjv-@4(4GzCyA`kS_y6l_~>6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0 zx&hUm^K@|xskoK&=l_3uW>p4-hKLE4Jv}`xGugIG5j@7mp&*uW_Q)=_Rbn|D4h)SE z8nYU6oVFdl+Q`Gmp-`f4uw(uo&l6>ADbtx)1cdF59+Q?nC?U_lp`cbXEBbT9L5cZ_ z42(ydyrwk9hJ>=Fg)_c<_3G7s2F4blju(jx#jG45{=a&s@(MWk@Q5%qFdTZtlwyB4 z=MXCcQwL}K13_j6CXV&u4s8v8y7?Jc1pM>`UV~LNZT|O_CAW>Sfg#D!|M`YJ8-#^! ev&1q9BryC~nwHJ_RIwiDGX_srKbLh*2~7aE7Hr-C literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png new file mode 100644 index 0000000000000000000000000000000000000000..35b9a4617677ab3b298e77d14d595c7e77840334 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`0iG_7Ar`&K2@+2d{;P4QMStG+ zU!doyiynE)rgBR{Cj5kako3Wf_sF}4<`cP9WBhLXIXE9|d0r3u>lo>X6 kcn%10pJ-TfVYx8_!zopr0B>|Hj{pDw literal 0 HcmV?d00001 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..ca3b6ede84b27981a41f8404b6e68997908dc550 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`QJyZ2Ar`&K2@4E=_5A5k6L&Cu#Cn6rVdLD!#zw~YhyYtLgB3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java new file mode 100644 index 000000000..77417412c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -0,0 +1,1415 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt; + +import com.android.ddmuilib.StackTracePanel; +import com.android.ddmuilib.StackTracePanel.ISourceRevealer; +import com.android.ddmuilib.console.DdmConsole; +import com.android.ddmuilib.console.IDdmConsole; +import com.android.ide.eclipse.adt.internal.VersionCheck; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; +import com.android.ide.eclipse.adt.internal.editors.menu.MenuEditor; +import com.android.ide.eclipse.adt.internal.editors.resources.ResourcesEditor; +import com.android.ide.eclipse.adt.internal.editors.xml.XmlEditor; +import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchController; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ExportHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ExportHelper.IExportCallback; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; +import com.android.ide.eclipse.adt.internal.sdk.LoadStatus; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.adt.internal.ui.EclipseUiHelper; +import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ide.eclipse.ddms.ImageLoader; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; +import com.android.sdkstats.SdkStatsService; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Preferences; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.core.runtime.Preferences.IPropertyChangeListener; +import org.eclipse.core.runtime.Preferences.PropertyChangeEvent; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorDescriptor; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleConstants; +import org.eclipse.ui.console.MessageConsole; +import org.eclipse.ui.console.MessageConsoleStream; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + +/** + * The activator class controls the plug-in life cycle + */ +@SuppressWarnings("deprecation") +public class AdtPlugin extends AbstractUIPlugin { + /** The plug-in ID */ + public static final String PLUGIN_ID = "com.android.ide.eclipse.adt"; //$NON-NLS-1$ + + /** singleton instance */ + private static AdtPlugin sPlugin; + + private static Image sAndroidLogo; + private static ImageDescriptor sAndroidLogoDesc; + + /** The global android console */ + private MessageConsole mAndroidConsole; + + /** Stream to write in the android console */ + private MessageConsoleStream mAndroidConsoleStream; + + /** Stream to write error messages to the android console */ + private MessageConsoleStream mAndroidConsoleErrorStream; + + /** Image loader object */ + private ImageLoader mLoader; + + /** Color used in the error console */ + private Color mRed; + + /** Load status of the SDK. Any access MUST be in a synchronized(mPostLoadProjects) block */ + private LoadStatus mSdkIsLoaded = LoadStatus.LOADING; + /** Project to update once the SDK is loaded. + * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */ + private final ArrayList mPostLoadProjectsToResolve = + new ArrayList(); + /** Project to check validity of cache vs actual once the SDK is loaded. + * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */ + private final ArrayList mPostLoadProjectsToCheck = new ArrayList(); + + private GlobalProjectMonitor mResourceMonitor; + private ArrayList mTargetChangeListeners = + new ArrayList(); + + protected boolean mSdkIsLoading; + + /** + * Custom PrintStream for Dx output. This class overrides the method + * println() and adds the standard output tag with the + * date and the project name in front of every messages. + */ + private static final class AndroidPrintStream extends PrintStream { + private IProject mProject; + private String mPrefix; + + /** + * Default constructor with project and output stream. + * The project is used to get the project name for the output tag. + * + * @param project The Project + * @param prefix A prefix to be printed before the actual message. Can be null + * @param stream The Stream + */ + public AndroidPrintStream(IProject project, String prefix, OutputStream stream) { + super(stream); + mProject = project; + } + + @Override + public void println(String message) { + // write the date/project tag first. + String tag = getMessageTag(mProject != null ? mProject.getName() : null); + + print(tag); + print(' '); + if (mPrefix != null) { + print(mPrefix); + } + + // then write the regular message + super.println(message); + } + } + + /** + * An error handler for checkSdkLocationAndId() that will handle the generated error + * or warning message. Each method must return a boolean that will in turn be returned by + * checkSdkLocationAndId. + */ + public static abstract class CheckSdkErrorHandler { + /** Handle an error message during sdk location check. Returns whatever + * checkSdkLocationAndId() should returns. + */ + public abstract boolean handleError(String message); + + /** Handle a warning message during sdk location check. Returns whatever + * checkSdkLocationAndId() should returns. + */ + public abstract boolean handleWarning(String message); + } + + /** + * The constructor + */ + public AdtPlugin() { + sPlugin = this; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + + Display display = getDisplay(); + + // set the default android console. + mAndroidConsole = new MessageConsole("Android", null); //$NON-NLS-1$ + ConsolePlugin.getDefault().getConsoleManager().addConsoles( + new IConsole[] { mAndroidConsole }); + + // get the stream to write in the android console. + mAndroidConsoleStream = mAndroidConsole.newMessageStream(); + mAndroidConsoleErrorStream = mAndroidConsole.newMessageStream(); + mRed = new Color(display, 0xFF, 0x00, 0x00); + + // because this can be run, in some cases, by a non ui thread, and beccause + // changing the console properties update the ui, we need to make this change + // in the ui thread. + display.asyncExec(new Runnable() { + public void run() { + mAndroidConsoleErrorStream.setColor(mRed); + } + }); + + // set up the ddms console to use this objects + DdmConsole.setConsole(new IDdmConsole() { + public void printErrorToConsole(String message) { + AdtPlugin.printErrorToConsole((String)null, message); + } + public void printErrorToConsole(String[] messages) { + AdtPlugin.printErrorToConsole((String)null, (Object[])messages); + } + public void printToConsole(String message) { + AdtPlugin.printToConsole((String)null, message); + } + public void printToConsole(String[] messages) { + AdtPlugin.printToConsole((String)null, (Object[])messages); + } + }); + + // get the eclipse store + AdtPrefs.init(getPreferenceStore()); + + // set the listener for the preference change + Preferences prefs = getPluginPreferences(); + prefs.addPropertyChangeListener(new IPropertyChangeListener() { + public void propertyChange(PropertyChangeEvent event) { + // load the new preferences + AdtPrefs.getPrefs().loadValues(event); + + // if the SDK changed, we have to do some extra work + if (AdtPrefs.PREFS_SDK_DIR.equals(event.getProperty())) { + + // finally restart adb, in case it's a different version + DdmsPlugin.setAdb(getOsAbsoluteAdb(), true /* startAdb */); + + // get the SDK location and build id. + if (checkSdkLocationAndId()) { + // if sdk if valid, reparse it + + reparseSdk(); + } + } + } + }); + + // load preferences. + AdtPrefs.getPrefs().loadValues(null /*event*/); + + // check the location of SDK + final boolean isSdkLocationValid = checkSdkLocationAndId(); + + // create the loader that's able to load the images + mLoader = new ImageLoader(this); + + // start the DdmsPlugin by setting the adb location, only if it is set already. + String osSdkLocation = AdtPrefs.getPrefs().getOsSdkFolder(); + if (osSdkLocation.length() > 0) { + DdmsPlugin.setAdb(getOsAbsoluteAdb(), true); + } + + // and give it the debug launcher for android projects + DdmsPlugin.setRunningAppDebugLauncher(new DdmsPlugin.IDebugLauncher() { + public boolean debug(String appName, int port) { + // search for an android project matching the process name + IProject project = ProjectHelper.findAndroidProjectByAppName(appName); + if (project != null) { + AndroidLaunchController.debugRunningApp(project, port); + return true; + } else { + return false; + } + } + }); + + StackTracePanel.setSourceRevealer(new ISourceRevealer() { + public void reveal(String applicationName, String className, int line) { + IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName); + if (project != null) { + BaseProjectHelper.revealSource(project, className, line); + } + } + }); + + // setup export callback for editors + ExportHelper.setCallback(new IExportCallback() { + public void startExportWizard(IProject project) { + StructuredSelection selection = new StructuredSelection(project); + + ExportWizard wizard = new ExportWizard(); + wizard.init(PlatformUI.getWorkbench(), selection); + WizardDialog dialog = new WizardDialog(getDisplay().getActiveShell(), + wizard); + dialog.open(); + } + }); + + // initialize editors + startEditors(); + + // Ping the usage server and parse the SDK content. + // This is deferred in separate jobs to avoid blocking the bundle start. + // We also serialize them to avoid too many parallel jobs when Eclipse starts. + Job pingJob = createPingUsageServerJob(); + pingJob.addJobChangeListener(new JobChangeAdapter() { + @Override + public void done(IJobChangeEvent event) { + super.done(event); + + // Once the ping job is finished, start the SDK parser + if (isSdkLocationValid) { + // parse the SDK resources. + parseSdkContent(); + } + } + }); + // build jobs are run after other interactive jobs + pingJob.setPriority(Job.BUILD); + // Wait 2 seconds before starting the ping job. This leaves some time to the + // other bundles to initialize. + pingJob.schedule(2000 /*milliseconds*/); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + @Override + public void stop(BundleContext context) throws Exception { + super.stop(context); + + stopEditors(); + + mRed.dispose(); + synchronized (AdtPlugin.class) { + sPlugin = null; + } + } + + /** Return the image loader for the plugin */ + public static synchronized ImageLoader getImageLoader() { + if (sPlugin != null) { + return sPlugin.mLoader; + } + return null; + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static synchronized AdtPlugin getDefault() { + return sPlugin; + } + + public static Display getDisplay() { + IWorkbench bench = null; + synchronized (AdtPlugin.class) { + bench = sPlugin.getWorkbench(); + } + + if (bench != null) { + return bench.getDisplay(); + } + return null; + } + + /** Returns the adb path relative to the sdk folder */ + public static String getOsRelativeAdb() { + return SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_ADB; + } + + /** Returns the zipalign path relative to the sdk folder */ + public static String getOsRelativeZipAlign() { + return SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_ZIPALIGN; + } + + /** Returns the emulator path relative to the sdk folder */ + public static String getOsRelativeEmulator() { + return SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_EMULATOR; + } + + /** Returns the absolute adb path */ + public static String getOsAbsoluteAdb() { + return getOsSdkFolder() + getOsRelativeAdb(); + } + + /** Returns the absolute zipalign path */ + public static String getOsAbsoluteZipAlign() { + return getOsSdkFolder() + getOsRelativeZipAlign(); + } + + /** Returns the absolute traceview path */ + public static String getOsAbsoluteTraceview() { + return getOsSdkFolder() + SdkConstants.OS_SDK_TOOLS_FOLDER + + AndroidConstants.FN_TRACEVIEW; + } + + /** Returns the absolute emulator path */ + public static String getOsAbsoluteEmulator() { + return getOsSdkFolder() + getOsRelativeEmulator(); + } + + /** + * Returns a Url file path to the javaDoc folder. + */ + public static String getUrlDoc() { + return ProjectHelper.getJavaDocPath( + getOsSdkFolder() + AndroidConstants.WS_JAVADOC_FOLDER_LEAF); + } + + /** + * Returns the SDK folder. + * Guaranteed to be terminated by a platform-specific path separator. + */ + public static synchronized String getOsSdkFolder() { + if (sPlugin == null) { + return null; + } + + return AdtPrefs.getPrefs().getOsSdkFolder(); + } + + public static String getOsSdkToolsFolder() { + return getOsSdkFolder() + SdkConstants.OS_SDK_TOOLS_FOLDER; + } + + /** + * Returns an image descriptor for the image file at the given + * plug-in relative path + * + * @param path the path + * @return the image descriptor + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } + + /** + * Reads and returns the content of a text file embedded in the plugin jar + * file. + * @param filepath the file path to the text file + * @return null if the file could not be read + */ + public static String readEmbeddedTextFile(String filepath) { + try { + InputStream is = readEmbeddedFileAsStream(filepath); + if (is != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + + String line; + StringBuilder total = new StringBuilder(reader.readLine()); + while ((line = reader.readLine()) != null) { + total.append('\n'); + total.append(line); + } + + return total.toString(); + } + } catch (IOException e) { + // we'll just return null;. + } + + return null; + } + + /** + * Reads and returns the content of a binary file embedded in the plugin jar + * file. + * @param filepath the file path to the text file + * @return null if the file could not be read + */ + public static byte[] readEmbeddedFile(String filepath) { + try { + InputStream is = readEmbeddedFileAsStream(filepath); + if (is != null) { + // create a buffered reader to facilitate reading. + BufferedInputStream stream = new BufferedInputStream(is); + + // get the size to read. + int avail = stream.available(); + + // create the buffer and reads it. + byte[] buffer = new byte[avail]; + stream.read(buffer); + + // and return. + return buffer; + } + } catch (IOException e) { + // we'll just return null;. + } + + return null; + } + + /** + * Reads and returns the content of a binary file embedded in the plugin jar + * file. + * @param filepath the file path to the text file + * @return null if the file could not be read + */ + public static InputStream readEmbeddedFileAsStream(String filepath) { + Bundle bundle = null; + synchronized (AdtPlugin.class) { + if (sPlugin != null) { + bundle = sPlugin.getBundle(); + } else { + return null; + } + } + + // attempt to get a file to one of the template. + try { + URL url = bundle.getEntry(AndroidConstants.WS_SEP + filepath); + if (url != null) { + return url.openStream(); + } + } catch (MalformedURLException e) { + // we'll just return null. + } catch (IOException e) { + // we'll just return null;. + } + + return null; + } + + /** + * Displays an error dialog box. This dialog box is ran asynchronously in the ui thread, + * therefore this method can be called from any thread. + * @param title The title of the dialog box + * @param message The error message + */ + public final static void displayError(final String title, final String message) { + // get the current Display + final Display display = getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, title, message); + } + }); + } + + /** + * Displays a warning dialog box. This dialog box is ran asynchronously in the ui thread, + * therefore this method can be called from any thread. + * @param title The title of the dialog box + * @param message The warning message + */ + public final static void displayWarning(final String title, final String message) { + // get the current Display + final Display display = getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openWarning(shell, title, message); + } + }); + } + + /** + * Display a yes/no question dialog box. This dialog is opened synchronously in the ui thread, + * therefore this message can be called from any thread. + * @param title The title of the dialog box + * @param message The error message + * @return true if OK was clicked. + */ + public final static boolean displayPrompt(final String title, final String message) { + // get the current Display and Shell + final Display display = getDisplay(); + + // we need to ask the user what he wants to do. + final boolean[] result = new boolean[1]; + display.syncExec(new Runnable() { + public void run() { + Shell shell = display.getActiveShell(); + result[0] = MessageDialog.openQuestion(shell, title, message); + } + }); + return result[0]; + } + + /** + * Logs a message to the default Eclipse log. + * + * @param severity The severity code. Valid values are: {@link IStatus#OK}, + * {@link IStatus#ERROR}, {@link IStatus#INFO}, {@link IStatus#WARNING} or + * {@link IStatus#CANCEL}. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static void log(int severity, String format, Object ... args) { + String message = String.format(format, args); + Status status = new Status(severity, PLUGIN_ID, message); + getDefault().getLog().log(status); + } + + /** + * Logs an exception to the default Eclipse log. + *

+ * The status severity is always set to ERROR. + * + * @param exception the exception to log. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static void log(Throwable exception, String format, Object ... args) { + String message = String.format(format, args); + Status status = new Status(IStatus.ERROR, PLUGIN_ID, message, exception); + getDefault().getLog().log(status); + } + + /** + * This is a mix between log(Throwable) and printErrorToConsole. + *

+ * This logs the exception with an ERROR severity and the given printf-like format message. + * The same message is then printed on the Android error console with the associated tag. + * + * @param exception the exception to log. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static synchronized void logAndPrintError(Throwable exception, String tag, + String format, Object ... args) { + if (sPlugin != null) { + String message = String.format(format, args); + Status status = new Status(IStatus.ERROR, PLUGIN_ID, message, exception); + getDefault().getLog().log(status); + printToStream(sPlugin.mAndroidConsoleErrorStream, tag, message); + showAndroidConsole(); + } + } + + /** + * Prints one or more error message to the android console. + * @param tag A tag to be associated with the message. Can be null. + * @param objects the objects to print through their toString method. + */ + public static synchronized void printErrorToConsole(String tag, Object... objects) { + if (sPlugin != null) { + printToStream(sPlugin.mAndroidConsoleErrorStream, tag, objects); + + showAndroidConsole(); + } + } + + /** + * Prints one or more error message to the android console. + * @param objects the objects to print through their toString method. + */ + public static void printErrorToConsole(Object... objects) { + printErrorToConsole((String)null, objects); + } + + /** + * Prints one or more error message to the android console. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their toString method. + */ + public static void printErrorToConsole(IProject project, Object... objects) { + String tag = project != null ? project.getName() : null; + printErrorToConsole(tag, objects); + } + + /** + * Prints one or more build messages to the android console, filtered by Build output verbosity. + * @param level {@link BuildVerbosity} level of the message. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their toString method. + * @see BuildVerbosity#ALWAYS + * @see BuildVerbosity#NORMAL + * @see BuildVerbosity#VERBOSE + */ + public static synchronized void printBuildToConsole(BuildVerbosity level, IProject project, + Object... objects) { + if (sPlugin != null) { + if (level.getLevel() <= AdtPrefs.getPrefs().getBuildVerbosity().getLevel()) { + String tag = project != null ? project.getName() : null; + printToStream(sPlugin.mAndroidConsoleStream, tag, objects); + } + } + } + + /** + * Prints one or more message to the android console. + * @param tag The tag to be associated with the message. Can be null. + * @param objects the objects to print through their toString method. + */ + public static synchronized void printToConsole(String tag, Object... objects) { + if (sPlugin != null) { + printToStream(sPlugin.mAndroidConsoleStream, tag, objects); + } + } + + /** + * Prints one or more message to the android console. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their toString method. + */ + public static void printToConsole(IProject project, Object... objects) { + String tag = project != null ? project.getName() : null; + printToConsole(tag, objects); + } + + /** Force the display of the android console */ + public static void showAndroidConsole() { + // first make sure the console is in the workbench + EclipseUiHelper.showView(IConsoleConstants.ID_CONSOLE_VIEW, true); + + // now make sure it's not docked. + ConsolePlugin.getDefault().getConsoleManager().showConsoleView( + AdtPlugin.getDefault().getAndroidConsole()); + } + + /** + * Returns an standard PrintStream object for a specific project.
+ * This PrintStream will add a date/project at the beginning of every + * println() output. + * + * @param project The project object + * @param prefix The prefix to be added to the message. Can be null. + * @return a new PrintStream + */ + public static synchronized PrintStream getOutPrintStream(IProject project, String prefix) { + if (sPlugin != null) { + return new AndroidPrintStream(project, prefix, sPlugin.mAndroidConsoleStream); + } + + return null; + } + + /** + * Returns an error PrintStream object for a specific project.
+ * This PrintStream will add a date/project at the beginning of every + * println() output. + * + * @param project The project object + * @param prefix The prefix to be added to the message. Can be null. + * @return a new PrintStream + */ + public static synchronized PrintStream getErrPrintStream(IProject project, String prefix) { + if (sPlugin != null) { + return new AndroidPrintStream(project, prefix, sPlugin.mAndroidConsoleErrorStream); + } + + return null; + } + + /** + * Returns whether the {@link IAndroidTarget}s have been loaded from the SDK. + */ + public final LoadStatus getSdkLoadStatus() { + synchronized (getSdkLockObject()) { + return mSdkIsLoaded; + } + } + + /** + * Returns the lock object for SDK loading. If you wish to do things while the SDK is loading, + * you must synchronize on this object. + */ + public final Object getSdkLockObject() { + return mPostLoadProjectsToResolve; + } + + /** + * Sets the given {@link IJavaProject} to have its target resolved again once the SDK finishes + * to load. + */ + public final void setProjectToResolve(IJavaProject javaProject) { + synchronized (getSdkLockObject()) { + mPostLoadProjectsToResolve.add(javaProject); + } + } + + /** + * Sets the given {@link IJavaProject} to have its target checked for consistency + * once the SDK finishes to load. This is used if the target is resolved using cached + * information while the SDK is loading. + */ + public final void setProjectToCheck(IJavaProject javaProject) { + // only lock on + synchronized (getSdkLockObject()) { + mPostLoadProjectsToCheck.add(javaProject); + } + } + + /** + * Checks the location of the SDK is valid and if it is, grab the SDK API version + * from the SDK. + * @return false if the location is not correct. + */ + private boolean checkSdkLocationAndId() { + String sdkLocation = AdtPrefs.getPrefs().getOsSdkFolder(); + if (sdkLocation == null || sdkLocation.length() == 0) { + displayError(Messages.Dialog_Title_SDK_Location, Messages.SDK_Not_Setup); + return false; + } + + return checkSdkLocationAndId(sdkLocation, new CheckSdkErrorHandler() { + @Override + public boolean handleError(String message) { + AdtPlugin.displayError(Messages.Dialog_Title_SDK_Location, + String.format(Messages.Error_Check_Prefs, message)); + return false; + } + + @Override + public boolean handleWarning(String message) { + AdtPlugin.displayWarning(Messages.Dialog_Title_SDK_Location, message); + return true; + } + }); + } + + /** + * Internal helper to perform the actual sdk location and id check. + * + * @param osSdkLocation The sdk directory, an OS path. + * @param errorHandler An checkSdkErrorHandler that can display a warning or an error. + * @return False if there was an error or the result from the errorHandler invocation. + */ + public boolean checkSdkLocationAndId(String osSdkLocation, CheckSdkErrorHandler errorHandler) { + if (osSdkLocation.endsWith(File.separator) == false) { + osSdkLocation = osSdkLocation + File.separator; + } + + File osSdkFolder = new File(osSdkLocation); + if (osSdkFolder.isDirectory() == false) { + return errorHandler.handleError( + String.format(Messages.Could_Not_Find_Folder, osSdkLocation)); + } + + String osTools = osSdkLocation + SdkConstants.OS_SDK_TOOLS_FOLDER; + File toolsFolder = new File(osTools); + if (toolsFolder.isDirectory() == false) { + return errorHandler.handleError( + String.format(Messages.Could_Not_Find_Folder_In_SDK, + SdkConstants.FD_TOOLS, osSdkLocation)); + } + + // check the path to various tools we use + String[] filesToCheck = new String[] { + osSdkLocation + getOsRelativeAdb(), + osSdkLocation + getOsRelativeEmulator() + }; + for (String file : filesToCheck) { + if (checkFile(file) == false) { + return errorHandler.handleError(String.format(Messages.Could_Not_Find, file)); + } + } + + // check the SDK build id/version and the plugin version. + return VersionCheck.checkVersion(osSdkLocation, errorHandler); + } + + /** + * Checks if a path reference a valid existing file. + * @param osPath the os path to check. + * @return true if the file exists and is, in fact, a file. + */ + private boolean checkFile(String osPath) { + File file = new File(osPath); + if (file.isFile() == false) { + return false; + } + + return true; + } + + /** + * Creates a job than can ping the usage server. + */ + private Job createPingUsageServerJob() { + // In order to not block the plugin loading, so we spawn another thread. + Job job = new Job("Android SDK Ping") { // Job name, visible in progress view + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + pingUsageServer(); //$NON-NLS-1$ + + return Status.OK_STATUS; + } catch (Throwable t) { + log(t, "pingUsageServer failed"); //$NON-NLS-1$ + return new Status(IStatus.ERROR, PLUGIN_ID, + "pingUsageServer failed", t); //$NON-NLS-1$ + } + } + }; + return job; + } + + /** + * Parses the SDK resources. + */ + private void parseSdkContent() { + // Perform the update in a thread (here an Eclipse runtime job) + // since this should never block the caller (especially the start method) + Job job = new Job(Messages.AdtPlugin_Android_SDK_Content_Loader) { + @SuppressWarnings("unchecked") + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + + if (mSdkIsLoading) { + return new Status(IStatus.WARNING, PLUGIN_ID, + "An Android SDK is already being loaded. Please try again later."); + } + + mSdkIsLoading = true; + + SubMonitor progress = SubMonitor.convert(monitor, + "Initialize SDK Manager", 100); + + Sdk sdk = Sdk.loadSdk(AdtPrefs.getPrefs().getOsSdkFolder()); + + if (sdk != null) { + + ArrayList list = new ArrayList(); + synchronized (getSdkLockObject()) { + mSdkIsLoaded = LoadStatus.LOADED; + + progress.setTaskName("Check Projects"); + + for (IJavaProject javaProject : mPostLoadProjectsToResolve) { + if (javaProject.getProject().isOpen()) { + list.add(javaProject); + } + } + + // done with this list. + mPostLoadProjectsToResolve.clear(); + } + + // check the projects that need checking. + // The method modifies the list (it removes the project that + // do not need to be resolved again). + AndroidClasspathContainerInitializer.checkProjectsCache( + mPostLoadProjectsToCheck); + + list.addAll(mPostLoadProjectsToCheck); + + // update the project that needs recompiling. + if (list.size() > 0) { + IJavaProject[] array = list.toArray( + new IJavaProject[list.size()]); + AndroidClasspathContainerInitializer.updateProjects(array); + } + + progress.worked(10); + } else { + // SDK failed to Load! + // Sdk#loadSdk() has already displayed an error. + synchronized (getSdkLockObject()) { + mSdkIsLoaded = LoadStatus.FAILED; + } + } + + // Notify resource changed listeners + progress.setTaskName("Refresh UI"); + progress.setWorkRemaining(mTargetChangeListeners.size()); + + // Clone the list before iterating, to avoid ConcurrentModification + // exceptions + final List listeners = + (List)mTargetChangeListeners.clone(); + final SubMonitor progress2 = progress; + AdtPlugin.getDisplay().asyncExec(new Runnable() { + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onSdkLoaded(); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } finally { + progress2.worked(1); + } + } + } + }); + } catch (Throwable t) { + log(t, "Unknown exception in parseSdkContent."); //$NON-NLS-1$ + return new Status(IStatus.ERROR, PLUGIN_ID, + "parseSdkContent failed", t); //$NON-NLS-1$ + + } finally { + mSdkIsLoading = false; + if (monitor != null) { + monitor.done(); + } + } + + return Status.OK_STATUS; + } + }; + job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs + job.schedule(); + } + + /** Returns the global android console */ + public MessageConsole getAndroidConsole() { + return mAndroidConsole; + } + + // ----- Methods for Editors ------- + + public void startEditors() { + sAndroidLogoDesc = imageDescriptorFromPlugin(AdtPlugin.PLUGIN_ID, + "/icons/android.png"); //$NON-NLS-1$ + sAndroidLogo = sAndroidLogoDesc.createImage(); + + // Add a resource listener to handle compiled resources. + IWorkspace ws = ResourcesPlugin.getWorkspace(); + mResourceMonitor = GlobalProjectMonitor.startMonitoring(ws); + + if (mResourceMonitor != null) { + try { + setupDefaultEditor(mResourceMonitor); + ResourceManager.setup(mResourceMonitor); + } catch (Throwable t) { + log(t, "ResourceManager.setup failed"); //$NON-NLS-1$ + } + } + } + + /** + * The AbstractUIPlugin implementation of this Plugin + * method saves this plug-in's preference and dialog stores and shuts down + * its image registry (if they are in use). Subclasses may extend this + * method, but must send super last. A try-finally statement should + * be used where necessary to ensure that super.shutdown() is + * always done. + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + public void stopEditors() { + sAndroidLogo.dispose(); + + IconFactory.getInstance().Dispose(); + + // Remove the resource listener that handles compiled resources. + IWorkspace ws = ResourcesPlugin.getWorkspace(); + GlobalProjectMonitor.stopMonitoring(ws); + + mRed.dispose(); + } + + /** + * Returns an Image for the small Android logo. + * + * Callers should not dispose it. + */ + public static Image getAndroidLogo() { + return sAndroidLogo; + } + + /** + * Returns an {@link ImageDescriptor} for the small Android logo. + * + * Callers should not dispose it. + */ + public static ImageDescriptor getAndroidLogoDesc() { + return sAndroidLogoDesc; + } + + /** + * Returns the ResourceMonitor object. + */ + public GlobalProjectMonitor getResourceMonitor() { + return mResourceMonitor; + } + + /** + * Sets up the editor to register default editors for resource files when needed. + * + * This is called by the {@link AdtPlugin} during initialization. + * + * @param monitor The main Resource Monitor object. + */ + public void setupDefaultEditor(GlobalProjectMonitor monitor) { + monitor.addFileListener(new IFileListener() { + + private static final String UNKNOWN_EDITOR = "unknown-editor"; //$NON-NLS-1$ + + /* (non-Javadoc) + * Sent when a file changed. + * @param file The file that changed. + * @param markerDeltas The marker deltas for the file. + * @param kind The change kind. This is equivalent to + * {@link IResourceDelta#accept(IResourceDeltaVisitor)} + * + * @see IFileListener#fileChanged + */ + public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) { + if (AndroidConstants.EXT_XML.equals(file.getFileExtension())) { + // The resources files must have a file path similar to + // project/res/.../*.xml + // There is no support for sub folders, so the segment count must be 4 + if (file.getFullPath().segmentCount() == 4) { + // check if we are inside the res folder. + String segment = file.getFullPath().segment(1); + if (segment.equalsIgnoreCase(SdkConstants.FD_RESOURCES)) { + // we are inside a res/ folder, get the actual ResourceFolder + ProjectResources resources = ResourceManager.getInstance(). + getProjectResources(file.getProject()); + + // This happens when importing old Android projects in Eclipse + // that lack the container (probably because resources fail to build + // properly.) + if (resources == null) { + log(IStatus.INFO, + "getProjectResources failed for path %1$s in project %2$s", //$NON-NLS-1$ + file.getFullPath().toOSString(), + file.getProject().getName()); + return; + } + + ResourceFolder resFolder = resources.getResourceFolder( + (IFolder)file.getParent()); + + if (resFolder != null) { + if (kind == IResourceDelta.ADDED) { + resourceAdded(file, resFolder.getType()); + } else if (kind == IResourceDelta.CHANGED) { + resourceChanged(file, resFolder.getType()); + } + } else { + // if the res folder is null, this means the name is invalid, + // in this case we remove whatever android editors that was set + // as the default editor. + IEditorDescriptor desc = IDE.getDefaultEditor(file); + String editorId = desc.getId(); + if (editorId.startsWith(AndroidConstants.EDITORS_NAMESPACE)) { + // reset the default editor. + IDE.setDefaultEditor(file, null); + } + } + } + } + } + } + + private void resourceAdded(IFile file, ResourceFolderType type) { + // set the default editor based on the type. + if (type == ResourceFolderType.LAYOUT) { + IDE.setDefaultEditor(file, LayoutEditor.ID); + } else if (type == ResourceFolderType.DRAWABLE + || type == ResourceFolderType.VALUES) { + IDE.setDefaultEditor(file, ResourcesEditor.ID); + } else if (type == ResourceFolderType.MENU) { + IDE.setDefaultEditor(file, MenuEditor.ID); + } else if (type == ResourceFolderType.XML) { + if (XmlEditor.canHandleFile(file)) { + IDE.setDefaultEditor(file, XmlEditor.ID); + } else { + // set a property to determine later if the XML can be handled + QualifiedName qname = new QualifiedName( + AdtPlugin.PLUGIN_ID, + UNKNOWN_EDITOR); + try { + file.setPersistentProperty(qname, "1"); //$NON-NLS-1$ + } catch (CoreException e) { + // pass + } + } + } + } + + private void resourceChanged(IFile file, ResourceFolderType type) { + if (type == ResourceFolderType.XML) { + IEditorDescriptor ed = IDE.getDefaultEditor(file); + if (ed == null || ed.getId() != XmlEditor.ID) { + QualifiedName qname = new QualifiedName( + AdtPlugin.PLUGIN_ID, + UNKNOWN_EDITOR); + String prop = null; + try { + prop = file.getPersistentProperty(qname); + } catch (CoreException e) { + // pass + } + if (prop != null && XmlEditor.canHandleFile(file)) { + try { + // remove the property & set editor + file.setPersistentProperty(qname, null); + + // the window can be null sometimes + IWorkbench wb = PlatformUI.getWorkbench(); + IWorkbenchWindow win = wb == null ? null : + wb.getActiveWorkbenchWindow(); + IWorkbenchPage page = win == null ? null : + win.getActivePage(); + + IEditorPart oldEditor = page == null ? null : + page.findEditor(new FileEditorInput(file)); + if (page != null && + oldEditor != null && + AdtPlugin.displayPrompt("Android XML Editor", + String.format("The file you just saved as been recognized as a file that could be better handled using the Android XML Editor. Do you want to edit '%1$s' using the Android XML editor instead?", + file.getFullPath()))) { + IDE.setDefaultEditor(file, XmlEditor.ID); + IEditorPart newEditor = page.openEditor( + new FileEditorInput(file), + XmlEditor.ID, + true, /* activate */ + IWorkbenchPage.MATCH_NONE); + + if (newEditor != null) { + page.closeEditor(oldEditor, true /* save */); + } + } + } catch (CoreException e) { + // setPersistentProperty or page.openEditor may have failed + } + } + } + } + } + + }, IResourceDelta.ADDED | IResourceDelta.CHANGED); + } + + /** + * Adds a new {@link ITargetChangeListener} to be notified when a new SDK is loaded, or when + * a project has its target changed. + */ + public void addTargetListener(ITargetChangeListener listener) { + mTargetChangeListeners.add(listener); + } + + /** + * Removes an existing {@link ITargetChangeListener}. + * @see #addTargetListener(ITargetChangeListener) + */ + public void removeTargetListener(ITargetChangeListener listener) { + mTargetChangeListeners.remove(listener); + } + + /** + * Updates all the {@link ITargetChangeListener}s that a target has changed for a given project. + *

Only editors related to that project should reload. + */ + @SuppressWarnings("unchecked") + public void updateTargetListeners(final IProject project) { + final List listeners = + (List)mTargetChangeListeners.clone(); + + AdtPlugin.getDisplay().asyncExec(new Runnable() { + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onProjectTargetChange(project); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } + } + } + }); + } + + /** + * Updates all the {@link ITargetChangeListener}s that a target data was loaded. + *

Only editors related to a project using this target should reload. + */ + @SuppressWarnings("unchecked") + public void updateTargetListeners(final IAndroidTarget target) { + final List listeners = + (List)mTargetChangeListeners.clone(); + + AdtPlugin.getDisplay().asyncExec(new Runnable() { + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onTargetLoaded(target); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } + } + } + }); + } + + public static synchronized OutputStream getErrorStream() { + return sPlugin.mAndroidConsoleErrorStream; + } + + /** + * Pings the usage start server. + */ + private void pingUsageServer() { + // get the version of the plugin + String versionString = (String) getBundle().getHeaders().get( + Constants.BUNDLE_VERSION); + Version version = new Version(versionString); + + versionString = String.format("%1$d.%2$d.%3$d", version.getMajor(), //$NON-NLS-1$ + version.getMinor(), version.getMicro()); + + SdkStatsService.ping("adt", versionString, getDisplay()); //$NON-NLS-1$ + } + + /** + * Reparses the content of the SDK and updates opened projects. + */ + public void reparseSdk() { + // add all the opened Android projects to the list of projects to be updated + // after the SDK is reloaded + synchronized (getSdkLockObject()) { + // get the project to refresh. + IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(); + mPostLoadProjectsToResolve.addAll(Arrays.asList(androidProjects)); + } + + // parse the SDK resources at the new location + parseSdkContent(); + } + + /** + * Prints messages, associated with a project to the specified stream + * @param stream The stream to write to + * @param tag The tag associated to the message. Can be null + * @param objects The objects to print through their toString() method (or directly for + * {@link String} objects. + */ + public static synchronized void printToStream(MessageConsoleStream stream, String tag, + Object... objects) { + String dateTag = getMessageTag(tag); + + for (Object obj : objects) { + stream.print(dateTag); + stream.print(" "); //$NON-NLS-1$ + if (obj instanceof String) { + stream.println((String)obj); + } else if (obj == null) { + stream.println("(null)"); //$NON-NLS-1$ + } else { + stream.println(obj.toString()); + } + } + } + + /** + * Creates a string containing the current date/time, and the tag. + * The tag does not end with a whitespace. + * @param tag The tag associated to the message. Can be null + * @return The dateTag + */ + public static String getMessageTag(String tag) { + Calendar c = Calendar.getInstance(); + + if (tag == null) { + return String.format(Messages.Console_Date_Tag, c); + } + + return String.format(Messages.Console_Data_Project_Tag, c, tag); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java new file mode 100644 index 000000000..9578a1da3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt; + +import com.android.ide.eclipse.adt.internal.build.ApkBuilder; +import com.android.ide.eclipse.adt.internal.build.PreCompilerBuilder; +import com.android.ide.eclipse.adt.internal.build.ResourceManagerBuilder; +import com.android.sdklib.SdkConstants; + +import java.io.File; +import java.util.regex.Pattern; + +/** + * Constant definition class.
+ *
+ * Most constants have a prefix defining the content. + *

    + *
  • WS_ Workspace path constant. Those are absolute paths, + * from the project root.
  • + *
  • OS_ OS path constant. These paths are different depending on the platform.
  • + *
  • FN_ File name constant.
  • + *
  • FD_ Folder name constant.
  • + *
  • MARKER_ Resource Marker Ids constant.
  • + *
  • EXT_ File extension constant. This does NOT include a dot.
  • + *
  • DOT_ File extension constant. This start with a dot.
  • + *
  • RE_ Regexp constant.
  • + *
  • NS_ Namespace constant.
  • + *
  • CLASS_ Fully qualified class name.
  • + *
+ * + */ +public class AndroidConstants { + /** + * The old Editors Plugin ID. It is still used in some places for compatibility. + * Please do not use for new features. + */ + public static final String EDITORS_NAMESPACE = "com.android.ide.eclipse.editors"; // $NON-NLS-1$ + + /** Nature of android projects */ + public final static String NATURE = "com.android.ide.eclipse.adt.AndroidNature"; //$NON-NLS-1$ + + /** Separator for workspace path, i.e. "/". */ + public final static String WS_SEP = "/"; //$NON-NLS-1$ + /** Separator character for workspace path, i.e. '/'. */ + public final static char WS_SEP_CHAR = '/'; + + /** Extension of the Application package Files, i.e. "apk". */ + public final static String EXT_ANDROID_PACKAGE = "apk"; //$NON-NLS-1$ + /** Extension of java files, i.e. "java" */ + public final static String EXT_JAVA = "java"; //$NON-NLS-1$ + /** Extension of compiled java files, i.e. "class" */ + public final static String EXT_CLASS = "class"; //$NON-NLS-1$ + /** Extension of xml files, i.e. "xml" */ + public final static String EXT_XML = "xml"; //$NON-NLS-1$ + /** Extension of jar files, i.e. "jar" */ + public final static String EXT_JAR = "jar"; //$NON-NLS-1$ + /** Extension of aidl files, i.e. "aidl" */ + public final static String EXT_AIDL = "aidl"; //$NON-NLS-1$ + /** Extension of native libraries, i.e. "so" */ + public final static String EXT_NATIVE_LIB = "so"; //$NON-NLS-1$ + + private final static String DOT = "."; //$NON-NLS-1$ + + /** Dot-Extension of the Application package Files, i.e. ".apk". */ + public final static String DOT_ANDROID_PACKAGE = DOT + EXT_ANDROID_PACKAGE; + /** Dot-Extension of java files, i.e. ".java" */ + public final static String DOT_JAVA = DOT + EXT_JAVA; + /** Dot-Extension of compiled java files, i.e. ".class" */ + public final static String DOT_CLASS = DOT + EXT_CLASS; + /** Dot-Extension of xml files, i.e. ".xml" */ + public final static String DOT_XML = DOT + EXT_XML; + /** Dot-Extension of jar files, i.e. ".jar" */ + public final static String DOT_JAR = DOT + EXT_JAR; + /** Dot-Extension of aidl files, i.e. ".aidl" */ + public final static String DOT_AIDL = DOT + EXT_AIDL; + + /** Name of the manifest file, i.e. "AndroidManifest.xml". */ + public static final String FN_ANDROID_MANIFEST = "AndroidManifest.xml"; //$NON-NLS-1$ + + /** Name of the android sources directory */ + public static final String FD_ANDROID_SOURCES = "sources"; //$NON-NLS-1$ + + /** Resource java class filename, i.e. "R.java" */ + public final static String FN_RESOURCE_CLASS = "R.java"; //$NON-NLS-1$ + /** Resource class file filename, i.e. "R.class" */ + public final static String FN_COMPILED_RESOURCE_CLASS = "R.class"; //$NON-NLS-1$ + /** Manifest java class filename, i.e. "Manifest.java" */ + public final static String FN_MANIFEST_CLASS = "Manifest.java"; //$NON-NLS-1$ + /** Dex conversion output filname, i.e. "classes.dex" */ + public final static String FN_CLASSES_DEX = "classes.dex"; //$NON-NLS-1$ + /** Temporary packaged resources file name, i.e. "resources.ap_" */ + public final static String FN_RESOURCES_AP_ = "resources.ap_"; //$NON-NLS-1$ + /** Temporary packaged resources file name for a specific set of configuration */ + public final static String FN_RESOURCES_S_AP_ = "resources-%s.ap_"; //$NON-NLS-1$ + public final static Pattern PATTERN_RESOURCES_S_AP_ = + Pattern.compile("resources-.*\\.ap_", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + public final static String FN_TRACEVIEW = + (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ? + "traceview.exe" : "traceview"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** Absolute path of the workspace root, i.e. "/" */ + public final static String WS_ROOT = WS_SEP; + + /** Absolute path of the resource folder, eg "/res".
This is a workspace path. */ + public final static String WS_RESOURCES = WS_SEP + SdkConstants.FD_RESOURCES; + + /** Absolute path of the resource folder, eg "/assets".
This is a workspace path. */ + public final static String WS_ASSETS = WS_SEP + SdkConstants.FD_ASSETS; + + /** Leaf of the javaDoc folder. Does not start with a separator. */ + public final static String WS_JAVADOC_FOLDER_LEAF = SdkConstants.FD_DOCS + "/" + //$NON-NLS-1$ + SdkConstants.FD_DOCS_REFERENCE; + + /** Path of the samples directory relative to the sdk folder. + * This is an OS path, ending with a separator. + * FIXME: remove once the NPW is fixed. */ + public final static String OS_SDK_SAMPLES_FOLDER = SdkConstants.FD_SAMPLES + File.separator; + + public final static String RE_DOT = "\\."; //$NON-NLS-1$ + /** Regexp for java extension, i.e. "\.java$" */ + public final static String RE_JAVA_EXT = "\\.java$"; //$NON-NLS-1$ + /** Regexp for aidl extension, i.e. "\.aidl$" */ + public final static String RE_AIDL_EXT = "\\.aidl$"; //$NON-NLS-1$ + + /** Namespace pattern for the custom resource XML, i.e. "http://schemas.android.com/apk/res/%s" */ + public final static String NS_CUSTOM_RESOURCES = "http://schemas.android.com/apk/res/%1$s"; //$NON-NLS-1$ + + /** The old common plug-in ID. Please do not use for new features. */ + private static final String LEGACY_PLUGIN_ID = "com.android.ide.eclipse.common"; //$NON-NLS-1$ + + /** Generic marker for ADT errors, only to be used in the {@link ResourceManagerBuilder} */ + public final static String MARKER_ADT = AdtPlugin.PLUGIN_ID + ".adtProblem"; //$NON-NLS-1$ + + /** Marker for Android Target errors. + * This is not cleared on each like other markers. Instead, it's cleared + * when an AndroidClasspathContainerInitializer has succeeded in creating an + * AndroidClasspathContainer */ + public final static String MARKER_TARGET = AdtPlugin.PLUGIN_ID + ".targetProblem"; //$NON-NLS-1$ + + /** aapt marker error when running the compile command, only to be used + * in {@link PreCompilerBuilder} */ + public final static String MARKER_AAPT_COMPILE = LEGACY_PLUGIN_ID + ".aaptProblem"; //$NON-NLS-1$ + + /** XML marker error, only to be used in {@link PreCompilerBuilder} */ + public final static String MARKER_XML = LEGACY_PLUGIN_ID + ".xmlProblem"; //$NON-NLS-1$ + + /** aidl marker error, only to be used in {@link PreCompilerBuilder} */ + public final static String MARKER_AIDL = LEGACY_PLUGIN_ID + ".aidlProblem"; //$NON-NLS-1$ + + /** android marker error, only to be used in the Manifest parsing + * from the {@link PreCompilerBuilder} */ + public final static String MARKER_ANDROID = LEGACY_PLUGIN_ID + ".androidProblem"; //$NON-NLS-1$ + + /** aapt marker error when running the package command, only to be used in {@link ApkBuilder} */ + public final static String MARKER_AAPT_PACKAGE = LEGACY_PLUGIN_ID + ".aapt2Problem"; //$NON-NLS-1$ + + /** final packaging error marker, only to be used in {@link ApkBuilder} */ + public final static String MARKER_PACKAGING = AdtPlugin.PLUGIN_ID + ".packagingProblem"; //$NON-NLS-1$ + + + /** Name for the "type" marker attribute */ + public final static String MARKER_ATTR_TYPE = "android.type"; //$NON-NLS-1$ + /** Name for the "class" marker attribute */ + public final static String MARKER_ATTR_CLASS = "android.class"; //$NON-NLS-1$ + /** activity value for marker attribute "type" */ + public final static String MARKER_ATTR_TYPE_ACTIVITY = "activity"; //$NON-NLS-1$ + /** service value for marker attribute "type" */ + public final static String MARKER_ATTR_TYPE_SERVICE = "service"; //$NON-NLS-1$ + /** receiver value for marker attribute "type" */ + public final static String MARKER_ATTR_TYPE_RECEIVER = "receiver"; //$NON-NLS-1$ + /** provider value for marker attribute "type" */ + public final static String MARKER_ATTR_TYPE_PROVIDER = "provider"; //$NON-NLS-1$ + + public final static String CLASS_ACTIVITY = "android.app.Activity"; //$NON-NLS-1$ + public final static String CLASS_SERVICE = "android.app.Service"; //$NON-NLS-1$ + public final static String CLASS_BROADCASTRECEIVER = "android.content.BroadcastReceiver"; //$NON-NLS-1$ + public final static String CLASS_CONTENTPROVIDER = "android.content.ContentProvider"; //$NON-NLS-1$ + public final static String CLASS_INSTRUMENTATION = "android.app.Instrumentation"; //$NON-NLS-1$ + public final static String CLASS_INSTRUMENTATION_RUNNER = + "android.test.InstrumentationTestRunner"; //$NON-NLS-1$ + public final static String CLASS_BUNDLE = "android.os.Bundle"; //$NON-NLS-1$ + public final static String CLASS_R = "android.R"; //$NON-NLS-1$ + public final static String CLASS_MANIFEST_PERMISSION = "android.Manifest$permission"; //$NON-NLS-1$ + public final static String CLASS_INTENT = "android.content.Intent"; //$NON-NLS-1$ + public final static String CLASS_CONTEXT = "android.content.Context"; //$NON-NLS-1$ + public final static String CLASS_VIEW = "android.view.View"; //$NON-NLS-1$ + public final static String CLASS_VIEWGROUP = "android.view.ViewGroup"; //$NON-NLS-1$ + public final static String CLASS_NAME_LAYOUTPARAMS = "LayoutParams"; //$NON-NLS-1$ + public final static String CLASS_VIEWGROUP_LAYOUTPARAMS = + CLASS_VIEWGROUP + "$" + CLASS_NAME_LAYOUTPARAMS; //$NON-NLS-1$ + public final static String CLASS_NAME_FRAMELAYOUT = "FrameLayout"; //$NON-NLS-1$ + public final static String CLASS_FRAMELAYOUT = + "android.widget." + CLASS_NAME_FRAMELAYOUT; //$NON-NLS-1$ + public final static String CLASS_PREFERENCE = "android.preference.Preference"; //$NON-NLS-1$ + public final static String CLASS_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; //$NON-NLS-1$ + public final static String CLASS_PREFERENCES = + "android.preference." + CLASS_NAME_PREFERENCE_SCREEN; //$NON-NLS-1$ + public final static String CLASS_PREFERENCEGROUP = "android.preference.PreferenceGroup"; //$NON-NLS-1$ + public final static String CLASS_PARCELABLE = "android.os.Parcelable"; //$NON-NLS-1$ + + public final static String CLASS_BRIDGE = "com.android.layoutlib.bridge.Bridge"; //$NON-NLS-1$ + + /** + * Prefered compiler level, i.e. "1.5". + */ + public final static String COMPILER_COMPLIANCE_PREFERRED = "1.5"; //$NON-NLS-1$ + /** + * List of valid compiler level, i.e. "1.5" and "1.6" + */ + public final static String[] COMPILER_COMPLIANCE = { + "1.5", //$NON-NLS-1$ + "1.6", //$NON-NLS-1$ + }; + + /** The base URL where to find the Android class & manifest documentation */ + public static final String CODESITE_BASE_URL = "http://code.google.com/android"; //$NON-NLS-1$ + + public static final String LIBRARY_TEST_RUNNER = "android.test.runner"; // $NON-NLS-1$ +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java new file mode 100644 index 000000000..3288ddb66 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java @@ -0,0 +1,50 @@ + +package com.android.ide.eclipse.adt; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.messages"; //$NON-NLS-1$ + + public static String AdtPlugin_Android_SDK_Content_Loader; + + public static String AdtPlugin_Android_SDK_Resource_Parser; + + public static String AdtPlugin_Failed_To_Parse_s; + + public static String AdtPlugin_Failed_To_Start_s; + + public static String Console_Data_Project_Tag; + + public static String Console_Date_Tag; + + public static String Could_Not_Find; + + public static String Could_Not_Find_Folder; + + public static String Could_Not_Find_Folder_In_SDK; + + public static String Dialog_Title_SDK_Location; + + public static String Error_Check_Prefs; + + public static String SDK_Not_Setup; + + public static String VersionCheck_Plugin_Too_Old; + + public static String VersionCheck_Plugin_Version_Failed; + + public static String VersionCheck_SDK_Build_Too_Low; + + public static String VersionCheck_SDK_Milestone_Too_Low; + + public static String VersionCheck_Unable_To_Parse_Version_s; + + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/BaseViewRule.java new file mode 100755 index 000000000..9c07b1428 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/BaseViewRule.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.editors.layout.gscripts; + +import java.util.ArrayList; +import java.util.Map; + + +/** + * An {@link BaseViewRule} describes the GLE rules that apply to a given Layout or View object + * in the Graphical Layout Editor (GLE). + *

+ * Such a rule is implemented using a Groovy script located in the + * com.android.ide.eclipse.adt.internal.editors.layout.gre package or in a + * projects' /gscript folder for custom views. + *

+ * The Groovy script must be named using the fully qualified class name of the View or Layout, + * e.g. "android.widget.LinearLayout.groovy". If the rule engine can't find a groovy script + * for a given element, it will use the closest matching parent (e.g. View instead of ViewGroup). + *

+ * Rule instances are stateless. They are created once per View class to handle and are shared + * across platforms or editor instances. As such, rules methods should never cache editor-specific + * arguments that they might receive. + */ +public abstract class BaseViewRule implements IViewRule { + + public boolean onInitialize(String fqcn) { + // This base rule can handle any class. + return true; + } + + public void onDispose() { + // Nothing to dispose. + } + + public String getDisplayName() { + // Default is to not override the selection display name. + return null; + } + + public Map getDefaultAttributes() { + // The base rule does not have any custom default attributes. + return null; + } + + public ArrayList dropStart(INodeProxy targetNode) { + // By default the base view rule does not participate in element creation by drag'n'drop. + return null; + } + + public void dropFinish(String sourceFqcn, INodeProxy targetNode, + DropZone selectedZone, Point where) { + // Nothing to do, the base rule does not participate in drag'n'drop. + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/DropZone.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/DropZone.java new file mode 100755 index 000000000..7bb871620 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/DropZone.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.editors.layout.gscripts; + + +/** + * A drop zone, as returned by {@link IViewRule#dropStart(INodeProxy)}. + *

+ * A zone is characterized by its {@link #bounds} rectangle, which must be valid for the + * zone to be useful (i.e. with w>0 and h>0). The zone must lie in the bounds given by + * the {@link INodeProxy} and is in absolute canvas coordinates. + *

+ * No strong ordering properties are defined if zones overlap each other. + */ +public class DropZone { + + /** + * The rectangle (in absolute coordinates) of the drop zone. + * Never null, but the rectangle can be invalid. + */ + public final Rect bounds = new Rect(); + + /** + * An opaque object that the script can use for its own purpose, e.g. some pre-computed + * data or a closure that can be computed in dropStart() and used in dropFinish(). + */ + public Object data; + + /** + * Creates a new DropZone with an invalid bounds rectangle and a null data object. + */ + public DropZone() { + } + + /** + * Creates a new DropZone with a copy of the bounds rectangle and the given data object. + */ + public DropZone(Rect bounds, Object data) { + this.bounds.set(bounds); + this.data = data; + } + + @Override + public String toString() { + return String.format("DropZone <%s, %s>", bounds, data); + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INodeProxy.java new file mode 100755 index 000000000..3e6cb472b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INodeProxy.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.editors.layout.gscripts; + +import groovy.lang.Closure; + + + +public interface INodeProxy { + + /** + * Returns the bounds of this node. + *

+ * The bounds are valid when this node maps a view that is already rendered. + * Typically, if the node is the target of a drag'n'drop operation then you can be + * guaranteed that its bounds are known and thus are valid. + *

+ * However the bounds are invalid (e.g. not known yet) for new XML elements + * that have just been created by the {@link #createChild(String)} method. + * + * @return A non-null rectangle, in canvas coordinates. + */ + Rect getBounds(); + + // ---- XML Editing --- + + /** + * Absolutely all calls that are going to edit the XML must be wrapped + * by an editXml() call. This call creates both an undo context wrapper and an + * edit-XML wrapper. + * + * @param undoName The UI name that will be given to the undo action. + * @param closure The code to execute. + */ + void editXml(String undoName, final Closure closure); + + // TODO define an exception that methods below will throw if editXml() is not wrapping + // these calls. + + /** + * Creates a new XML element as a child of this node's XML element. + *

+ * For this to work, the editor must have a descriptor for the given FQCN. + *

+ * This call must be done in the context of editXml(). + * + * @param viewFqcn The FQCN of the element to create. The actual XML local name will + * depend on whether this is an Android view or a custom project view. + * @return The node for the newly created element. Can be null if we failed to create it. + */ + INodeProxy createChild(String viewFqcn); + + /** + * Sets an attribute for the underlying XML element. + * Attributes are not written immediately -- instead the XML editor batches edits and + * then commits them all together at once later. + *

+ * The attribute will only be set if the underlying element's descriptor is aware of + * this attribute. + *

+ * This call must be done in the context of editXml(). + * + * @param attributeName The XML local name of the attribute to set. + * @param value It's value. Cannot be null. + * @return Whether the attribute was actually set or not. + */ + boolean setAttribute(String attributeName, String value); + + + + // ----------- + + /** TODO: this is a hack. Shouldn't be here but instead part of some kind of helper + * given to IViewRule implementations. + */ + void debugPrintf(String msg, Object...params); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java new file mode 100755 index 000000000..73aa1b06f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.editors.layout.gscripts; + +import java.util.ArrayList; +import java.util.Map; + + +/** + * An {@link IViewRule} describes the GLE rules that apply to a given Layout or View object + * in the Graphical Layout Editor (GLE). + *

+ * Such a rule is implemented using a Groovy script located in the + * com.android.ide.eclipse.adt.internal.editors.layout.gre package or in a + * projects' /gscript folder for custom views. + *

+ * The Groovy script must be named using the fully qualified class name of the View or Layout, + * e.g. "android.widget.LinearLayout.groovy". If the rule engine can't find a groovy script + * for a given element, it will use the closest matching parent (e.g. View instead of ViewGroup). + *

+ * Rule instances are stateless. They are created once per View class to handle and are shared + * across platforms or editor instances. As such, rules methods should never cache editor-specific + * arguments that they might receive. + */ +public interface IViewRule { + + /** + * This method is called by the rule engine when the script is first loaded. + * It gives the rule a chance to initialize itself. + * + * @param fqcn The fully qualified class name of the Layout or View that will be managed by + * this rule. This can be cached as it will never change for the lifetime of this rule + * instance. This may or may not match the script's filename as it may be the fqcn of a + * class derived from the one this rule can handle. + * @return True if this rule can handle the given FQCN. False if the rule can't handle the + * given FQCN, in which case the rule engine will find another rule matching a parent clas. + */ + boolean onInitialize(String fqcn); + + /** + * This method is called by the rules engine just before the script is unloaded. + */ + void onDispose(); + + /** + * Returns the class name to display when an element is selected in the GLE. + *

+ * If null is returned, the GLE will automatically shorten the class name using its + * own heuristic, which is to keep the first 2 package components and the class name. + * The class name is the fqcn argument that was given + * to {@link #onInitialize(String)}. + * + * @return Null for the default behavior or a shortened string. + */ + String getDisplayName(); + + + // ==== XML Creation ==== + + + /** + * Returns the default attributes that a new XML element of this type should have + * when added to an XML layout file. Note that these defaults can be overridden by the + * specific code performing the insertion. + * + * @return A map of attribute:values for a new element of this type. Can be null or empty. + */ + Map getDefaultAttributes(); + + + // ==== Drag'n'drop support ==== + + + /** + * Called when a drop operation starts, whilst the d'n'd is dragging the cursor over the + * views. The purpose of the drop operation will be to create a new element. + *

+ * Drop targets that can't accept child views should always return null, in which case + * the rule engine will ask the parent view (typically a layout). + *

+ * Drop targets that can accept child views must return a non-empty list of drop zones, + * customized to the actual bounds of the target. + * The drop zones will be visually shown to the user. Once the user releases the mouse + * in one of the drop zone, the dropAccept/dropFinish methods will be called. + *

+ * Note that at this stage, the drop operation does not offer a way to know what is going + * to be dropped. We just know it's a view descriptor, typically from the layout palette, + * but we don't know which view class yet. + * + * @param targetNode The XML view that is currently the target of the drop. + * @return Null or an empty list if the rule rejects the drop, or a list of usable drop zones. + */ + ArrayList dropStart(INodeProxy targetNode); + + /** + * Called after the user selects to drop the given source into one of the drop zones. + *

+ * This method should use the methods from the {@link INodeProxy} to actually create the + * new XML matching the source descriptor. + * + * @param sourceFqcn The FQCN of the view being dropped. + * @param targetNode The XML view that is currently the target of the drop. + * @param selectedZone One of the drop zones returned by {@link #dropStart(INodeProxy)}. + * @param where The location, in the selected zone, of the drop. + */ + void dropFinish( + String sourceFqcn, + INodeProxy targetNode, + DropZone selectedZone, + Point where); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Point.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Point.java new file mode 100755 index 000000000..2e5791231 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Point.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.editors.layout.gscripts; + + +/** + * Mutable point. + */ +public class Point { + public int x, y; + + public Point(int x, int y) { + this.x = x; + this.y = y; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Rect.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Rect.java new file mode 100755 index 000000000..8d0c08c74 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Rect.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.editors.layout.gscripts; + + +/** + * Mutable rectangle bounds. + *

+ * To be valid, w >= 1 and h >= 1. + * By definition: + * - right side = x + w - 1. + * - bottom side = y + h - 1. + */ +public class Rect { + public int x, y, w, h; + + /** Initialize an invalid rectangle. */ + public Rect() { + } + + /** Initialize rectangle to the given values. They can be invalid. */ + public Rect(int x, int y, int w, int h) { + set(x, y, w, h); + } + + /** Initialize rectangle to the given values. They can be invalid. */ + public void set(int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + /** Initialize rectangle to match the given one. */ + public void set(Rect r) { + x = r.x; + y = r.y; + w = r.w; + h = r.h; + } + + /** Returns a new instance of a rectangle with the same values. */ + public Rect copy() { + return new Rect(x, y, w, h); + } + + /** Returns true if the rectangle has valid bounds, i.e. w>0 and h>0. */ + public boolean isValid() { + return w > 0 && h > 0; + } + + /** Returns true if the rectangle contains the x,y coordinates, borders included. */ + public boolean contains(int x, int y) { + return isValid() && + x >= this.x && + y >= this.y && + x < (this.x + this.w) && + y < (this.y + this.h); + } + + @Override + public String toString() { + return String.format("Rect [%dx%d - %dx%d]", x, y, w, h); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/VersionCheck.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/VersionCheck.java new file mode 100644 index 000000000..6199ef9e7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/VersionCheck.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.Messages; +import com.android.ide.eclipse.adt.AdtPlugin.CheckSdkErrorHandler; +import com.android.sdklib.SdkConstants; + +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class handling the version check for the plugin vs. the SDK.
+ * The plugin must be able to support all version of the SDK. + * + *

An SDK can require a new version of the plugin. + *

The SDK contains a file with the minimum version for the plugin. This file is inside the + * tools/lib directory, and is called plugin.prop.
+ * Inside that text file, there is a line in the format "plugin.version=#.#.#". This is checked + * against the current plugin version.
+ * + */ +public final class VersionCheck { + /** + * Pattern to get the minimum plugin version supported by the SDK. This is read from + * the file $SDK/tools/lib/plugin.prop. + */ + private final static Pattern sPluginVersionPattern = Pattern.compile( + "^plugin.version=(\\d+)\\.(\\d+)\\.(\\d+).*$"); //$NON-NLS-1$ + + /** + * Checks the plugin and the SDK have compatible versions. + * @param osSdkPath The path to the SDK + * @return true if compatible. + */ + public static boolean checkVersion(String osSdkPath, CheckSdkErrorHandler errorHandler) { + AdtPlugin plugin = AdtPlugin.getDefault(); + String osLibs = osSdkPath + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; + + // get the plugin property file, and grab the minimum plugin version required + // to work with the sdk + int minMajorVersion = -1; + int minMinorVersion = -1; + int minMicroVersion = -1; + try { + FileReader reader = new FileReader(osLibs + SdkConstants.FN_PLUGIN_PROP); + BufferedReader bReader = new BufferedReader(reader); + String line; + while ((line = bReader.readLine()) != null) { + Matcher m = sPluginVersionPattern.matcher(line); + if (m.matches()) { + minMajorVersion = Integer.parseInt(m.group(1)); + minMinorVersion = Integer.parseInt(m.group(2)); + minMicroVersion = Integer.parseInt(m.group(3)); + break; + } + } + } catch (FileNotFoundException e) { + // the build id will be null, and this is handled by the builders. + } catch (IOException e) { + // the build id will be null, and this is handled by the builders. + } + + // Failed to get the min plugin version number? + if (minMajorVersion == -1 || minMinorVersion == -1 || minMicroVersion ==-1) { + return errorHandler.handleWarning(Messages.VersionCheck_Plugin_Version_Failed); + } + + // test the plugin number + String versionString = (String) plugin.getBundle().getHeaders().get( + Constants.BUNDLE_VERSION); + Version version = new Version(versionString); + + boolean valid = true; + if (version.getMajor() < minMajorVersion) { + valid = false; + } else if (version.getMajor() == minMajorVersion) { + if (version.getMinor() < minMinorVersion) { + valid = false; + } else if (version.getMinor() == minMinorVersion) { + if (version.getMicro() < minMicroVersion) { + valid = false; + } + } + } + + if (valid == false) { + return errorHandler.handleWarning( + String.format(Messages.VersionCheck_Plugin_Too_Old, + minMajorVersion, minMinorVersion, minMicroVersion, versionString)); + } + + return true; // no error! + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/ConvertToAndroidAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/ConvertToAndroidAction.java new file mode 100644 index 000000000..cfbc93dbd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/ConvertToAndroidAction.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.actions; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; + +import java.util.Iterator; + +/** + * Converts a project created with the activity creator into an + * Android project. + */ +public class ConvertToAndroidAction implements IObjectActionDelegate { + + private ISelection mSelection; + + /* + * (non-Javadoc) + * + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + // pass + } + + /* + * (non-Javadoc) + * + * @see IActionDelegate#run(IAction) + */ + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + for (Iterator it = ((IStructuredSelection)mSelection).iterator(); it.hasNext();) { + Object element = it.next(); + IProject project = null; + if (element instanceof IProject) { + project = (IProject)element; + } else if (element instanceof IAdaptable) { + project = (IProject)((IAdaptable)element).getAdapter(IProject.class); + } + if (project != null) { + convertProject(project); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see IActionDelegate#selectionChanged(IAction, ISelection) + */ + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } + + /** + * Toggles sample nature on a project + * + * @param project to have sample nature added or removed + */ + private void convertProject(final IProject project) { + new Job("Convert Project") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (monitor != null) { + monitor.beginTask(String.format( + "Convert %1$s to Android", project.getName()), 5); + } + + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // check if the project already has the android nature. + for (int i = 0; i < natures.length; ++i) { + if (AndroidConstants.NATURE.equals(natures[i])) { + // we shouldn't be here as the visibility of the item + // is dependent on the project. + return new Status(Status.WARNING, AdtPlugin.PLUGIN_ID, + "Project is already an Android project"); + } + } + + if (monitor != null) { + monitor.worked(1); + } + + String[] newNatures = new String[natures.length + 1]; + System.arraycopy(natures, 0, newNatures, 1, natures.length); + newNatures[0] = AndroidConstants.NATURE; + + // set the new nature list in the project + description.setNatureIds(newNatures); + project.setDescription(description, null); + if (monitor != null) { + monitor.worked(1); + } + + // Fix the classpath entries. + // get a java project + IJavaProject javaProject = JavaCore.create(project); + ProjectHelper.fixProjectClasspathEntries(javaProject); + if (monitor != null) { + monitor.worked(1); + } + + return Status.OK_STATUS; + } catch (JavaModelException e) { + return e.getJavaModelStatus(); + } catch (CoreException e) { + return e.getStatus(); + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + }.schedule(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java new file mode 100644 index 000000000..cb4f7e74c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.actions; + +import com.android.ide.eclipse.adt.internal.project.AndroidNature; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; + +import java.util.Iterator; + +/** + * Action to fix the project properties: + *

    + *
  • Make sure the framework archive is present with the link to the java + * doc
  • + *
+ */ +public class FixProjectAction implements IObjectActionDelegate { + + private ISelection mSelection; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + } + + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + + for (Iterator it = ((IStructuredSelection) mSelection).iterator(); + it.hasNext();) { + Object element = it.next(); + IProject project = null; + if (element instanceof IProject) { + project = (IProject) element; + } else if (element instanceof IAdaptable) { + project = (IProject) ((IAdaptable) element) + .getAdapter(IProject.class); + } + if (project != null) { + fixProject(project); + } + } + } + } + + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } + + private void fixProject(final IProject project) { + new Job("Fix Project Properties") { + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (monitor != null) { + monitor.beginTask("Fix Project Properties", 6); + } + + ProjectHelper.fixProject(project); + if (monitor != null) { + monitor.worked(1); + } + + // fix the nature order to have the proper project icon + ProjectHelper.fixProjectNatureOrder(project); + if (monitor != null) { + monitor.worked(1); + } + + // now we fix the builders + AndroidNature.configureResourceManagerBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + AndroidNature.configurePreBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + AndroidNature.configureApkBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + return Status.OK_STATUS; + } catch (JavaModelException e) { + return e.getJavaModelStatus(); + } catch (CoreException e) { + return e.getStatus(); + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + }.schedule(); + } + + /** + * @see IWorkbenchWindowActionDelegate#init + */ + public void init(IWorkbenchWindow window) { + // pass + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java new file mode 100644 index 000000000..9315a1c9a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java @@ -0,0 +1,1318 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.build; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.ApkInstallManager; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.DexWrapper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.jarutils.DebugKeyProvider; +import com.android.jarutils.JavaResourceFilter; +import com.android.jarutils.SignedJarBuilder; +import com.android.jarutils.DebugKeyProvider.IKeyGenOutput; +import com.android.jarutils.DebugKeyProvider.KeytoolException; +import com.android.jarutils.SignedJarBuilder.IZipEntryFilter; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.internal.project.ApkSettings; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.preference.IPreferenceStore; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +public class ApkBuilder extends BaseBuilder { + + public static final String ID = "com.android.ide.eclipse.adt.ApkBuilder"; //$NON-NLS-1$ + + private static final String PROPERTY_CONVERT_TO_DEX = "convertToDex"; //$NON-NLS-1$ + private static final String PROPERTY_PACKAGE_RESOURCES = "packageResources"; //$NON-NLS-1$ + private static final String PROPERTY_BUILD_APK = "buildApk"; //$NON-NLS-1$ + + private static final String DX_PREFIX = "Dx"; //$NON-NLS-1$ + + private final static String GDBSERVER_NAME = "gdbserver"; //$NON-NLS-1$ + + /** + * Dex conversion flag. This is set to true if one of the changed/added/removed + * file is a .class file. Upon visiting all the delta resource, if this + * flag is true, then we know we'll have to make the "classes.dex" file. + */ + private boolean mConvertToDex = false; + + /** + * Package resources flag. This is set to true if one of the changed/added/removed + * file is a resource file. Upon visiting all the delta resource, if + * this flag is true, then we know we'll have to repackage the resources. + */ + private boolean mPackageResources = false; + + /** + * Final package build flag. + */ + private boolean mBuildFinalPackage = false; + + private PrintStream mOutStream = null; + private PrintStream mErrStream = null; + + /** + * Basic Resource Delta Visitor class to check if a referenced project had a change in its + * compiled java files. + */ + private static class ReferencedProjectDeltaVisitor implements IResourceDeltaVisitor { + + private boolean mConvertToDex = false; + private boolean mMakeFinalPackage; + + private IPath mOutputFolder; + private ArrayList mSourceFolders; + + private ReferencedProjectDeltaVisitor(IJavaProject javaProject) { + try { + mOutputFolder = javaProject.getOutputLocation(); + mSourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject); + } catch (JavaModelException e) { + } finally { + } + } + + /** + * {@inheritDoc} + * @throws CoreException + */ + public boolean visit(IResourceDelta delta) throws CoreException { + // no need to keep looking if we already know we need to convert + // to dex and make the final package. + if (mConvertToDex && mMakeFinalPackage) { + return false; + } + + // get the resource and the path segments. + IResource resource = delta.getResource(); + IPath resourceFullPath = resource.getFullPath(); + + if (mOutputFolder.isPrefixOf(resourceFullPath)) { + int type = resource.getType(); + if (type == IResource.FILE) { + String ext = resource.getFileExtension(); + if (AndroidConstants.EXT_CLASS.equals(ext)) { + mConvertToDex = true; + } + } + return true; + } else { + for (IPath sourceFullPath : mSourceFolders) { + if (sourceFullPath.isPrefixOf(resourceFullPath)) { + int type = resource.getType(); + if (type == IResource.FILE) { + // check if the file is a valid file that would be + // included during the final packaging. + if (checkFileForPackaging((IFile)resource)) { + mMakeFinalPackage = true; + } + + return false; + } else if (type == IResource.FOLDER) { + // if this is a folder, we check if this is a valid folder as well. + // If this is a folder that needs to be ignored, we must return false, + // so that we ignore its content. + return checkFolderForPackaging((IFolder)resource); + } + } + } + } + + return true; + } + + /** + * Returns if one of the .class file was modified. + */ + boolean needDexConvertion() { + return mConvertToDex; + } + + boolean needMakeFinalPackage() { + return mMakeFinalPackage; + } + } + + /** + * Custom {@link IZipEntryFilter} to filter out everything that is not a standard java + * resources, and also record whether the zip file contains native libraries. + *

Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when + * we only want the java resources from external jars. + */ + private final static class JavaAndNativeResourceFilter extends JavaResourceFilter { + private final List mNativeLibs = new ArrayList(); + private boolean mNativeLibInteference = false; + + @Override + public boolean checkEntry(String name) { + boolean value = super.checkEntry(name); + + // only do additional checks if the file passes the default checks. + if (value) { + if (name.endsWith(".so")) { + mNativeLibs.add(name); + + // only .so located in lib/ will interfer with the installation + if (name.startsWith("lib/")) { + mNativeLibInteference = true; + } + } else if (name.endsWith(".jnilib")) { + mNativeLibs.add(name); + } + } + + return value; + } + + List getNativeLibs() { + return mNativeLibs; + } + + boolean getNativeLibInterefence() { + return mNativeLibInteference; + } + + void clear() { + mNativeLibs.clear(); + mNativeLibInteference = false; + } + } + + private final JavaAndNativeResourceFilter mResourceFilter = new JavaAndNativeResourceFilter(); + + public ApkBuilder() { + super(); + } + + @Override + protected void clean(IProgressMonitor monitor) throws CoreException { + super.clean(monitor); + + // Get the project. + IProject project = getProject(); + + // Clear the project of the generic markers + removeMarkersFromProject(project, AndroidConstants.MARKER_AAPT_COMPILE); + removeMarkersFromProject(project, AndroidConstants.MARKER_PACKAGING); + } + + // build() returns a list of project from which this project depends for future compilation. + @SuppressWarnings({"unchecked"}) + @Override + protected IProject[] build(int kind, Map args, IProgressMonitor monitor) + throws CoreException { + // get a project object + IProject project = getProject(); + + // list of referenced projects. + IProject[] referencedProjects = null; + + try { + IJavaProject javaProject = JavaCore.create(project); + + // Top level check to make sure the build can move forward. + abortOnBadSetup(javaProject); + + // get the list of referenced projects. + referencedProjects = ProjectHelper.getReferencedProjects(project); + IJavaProject[] referencedJavaProjects = getJavaProjects(referencedProjects); + + // get the output folder, this method returns the path with a trailing + // separator + IFolder outputFolder = BaseProjectHelper.getOutputFolder(project); + + // now we need to get the classpath list + ArrayList sourceList = BaseProjectHelper.getSourceClasspaths(javaProject); + + // First thing we do is go through the resource delta to not + // lose it if we have to abort the build for any reason. + ApkDeltaVisitor dv = null; + if (kind == FULL_BUILD) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Full_Apk_Build); + + mPackageResources = true; + mConvertToDex = true; + mBuildFinalPackage = true; + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Inc_Apk_Build); + + // go through the resources and see if something changed. + IResourceDelta delta = getDelta(project); + if (delta == null) { + mPackageResources = true; + mConvertToDex = true; + mBuildFinalPackage = true; + } else { + dv = new ApkDeltaVisitor(this, sourceList, outputFolder); + delta.accept(dv); + + // save the state + mPackageResources |= dv.getPackageResources(); + mConvertToDex |= dv.getConvertToDex(); + mBuildFinalPackage |= dv.getMakeFinalPackage(); + } + + // also go through the delta for all the referenced projects, until we are forced to + // compile anyway + for (int i = 0 ; i < referencedJavaProjects.length && + (mBuildFinalPackage == false || mConvertToDex == false); i++) { + IJavaProject referencedJavaProject = referencedJavaProjects[i]; + delta = getDelta(referencedJavaProject.getProject()); + if (delta != null) { + ReferencedProjectDeltaVisitor refProjectDv = new ReferencedProjectDeltaVisitor( + referencedJavaProject); + delta.accept(refProjectDv); + + // save the state + mConvertToDex |= refProjectDv.needDexConvertion(); + mBuildFinalPackage |= refProjectDv.needMakeFinalPackage(); + } + } + } + + // store the build status in the persistent storage + saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , mConvertToDex); + saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources); + saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); + + if (dv != null && dv.mXmlError) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Xml_Error); + + // if there was some XML errors, we just return w/o doing + // anything since we've put some markers in the files anyway + return referencedProjects; + } + + // remove older packaging markers. + removeMarkersFromProject(javaProject.getProject(), AndroidConstants.MARKER_PACKAGING); + + if (outputFolder == null) { + // mark project and exit + markProject(AndroidConstants.MARKER_PACKAGING, Messages.Failed_To_Get_Output, + IMarker.SEVERITY_ERROR); + return referencedProjects; + } + + // first thing we do is check that the SDK directory has been setup. + String osSdkFolder = AdtPlugin.getOsSdkFolder(); + + if (osSdkFolder.length() == 0) { + // this has already been checked in the precompiler. Therefore, + // while we do have to cancel the build, we don't have to return + // any error or throw anything. + return referencedProjects; + } + + // get the APK configs for the project. + ApkSettings apkSettings = Sdk.getCurrent().getApkSettings(project); + Set> apkfilters = null; + if (apkSettings != null) { + Map filterMap = apkSettings.getResourceFilters(); + if (filterMap != null && filterMap.size() > 0) { + apkfilters = filterMap.entrySet(); + } + } + + // do some extra check, in case the output files are not present. This + // will force to recreate them. + IResource tmp = null; + + if (mPackageResources == false) { + // check the full resource package + tmp = outputFolder.findMember(AndroidConstants.FN_RESOURCES_AP_); + if (tmp == null || tmp.exists() == false) { + mPackageResources = true; + mBuildFinalPackage = true; + } else { + // if the full package is present, we check the filtered resource packages + // as well + if (apkfilters != null) { + for (Entry entry : apkfilters) { + String filename = String.format(AndroidConstants.FN_RESOURCES_S_AP_, + entry.getKey()); + + tmp = outputFolder.findMember(filename); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, + filename); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, + project, msg); + mPackageResources = true; + mBuildFinalPackage = true; + break; + } + } + } + } + } + + // check classes.dex is present. If not we force to recreate it. + if (mConvertToDex == false) { + tmp = outputFolder.findMember(AndroidConstants.FN_CLASSES_DEX); + if (tmp == null || tmp.exists() == false) { + mConvertToDex = true; + mBuildFinalPackage = true; + } + } + + // also check the final file(s)! + String finalPackageName = ProjectHelper.getApkFilename(project, null /*config*/); + if (mBuildFinalPackage == false) { + tmp = outputFolder.findMember(finalPackageName); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg); + mBuildFinalPackage = true; + } else if (apkfilters != null) { + // if the full apk is present, we check the filtered apk as well + for (Entry entry : apkfilters) { + String filename = ProjectHelper.getApkFilename(project, entry.getKey()); + + tmp = outputFolder.findMember(filename); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, filename); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg); + mBuildFinalPackage = true; + break; + } + } + } + } + + // at this point we know if we need to recreate the temporary apk + // or the dex file, but we don't know if we simply need to recreate them + // because they are missing + + // refresh the output directory first + IContainer ic = outputFolder.getParent(); + if (ic != null) { + ic.refreshLocal(IResource.DEPTH_ONE, monitor); + } + + // we need to test all three, as we may need to make the final package + // but not the intermediary ones. + if (mPackageResources || mConvertToDex || mBuildFinalPackage) { + IPath binLocation = outputFolder.getLocation(); + if (binLocation == null) { + markProject(AndroidConstants.MARKER_PACKAGING, Messages.Output_Missing, + IMarker.SEVERITY_ERROR); + return referencedProjects; + } + String osBinPath = binLocation.toOSString(); + + // Remove the old .apk. + // This make sure that if the apk is corrupted, then dx (which would attempt + // to open it), will not fail. + String osFinalPackagePath = osBinPath + File.separator + finalPackageName; + File finalPackage = new File(osFinalPackagePath); + + // if delete failed, this is not really a problem, as the final package generation + // handle already present .apk, and if that one failed as well, the user will be + // notified. + finalPackage.delete(); + + if (apkfilters != null) { + for (Entry entry : apkfilters) { + String packageFilepath = osBinPath + File.separator + + ProjectHelper.getApkFilename(project, entry.getKey()); + + finalPackage = new File(packageFilepath); + finalPackage.delete(); + } + } + + // first we check if we need to package the resources. + if (mPackageResources) { + // remove some aapt_package only markers. + removeMarkersFromContainer(project, AndroidConstants.MARKER_AAPT_PACKAGE); + + // need to figure out some path before we can execute aapt; + + // resource to the AndroidManifest.xml file + IResource manifestResource = project .findMember( + AndroidConstants.WS_SEP + AndroidConstants.FN_ANDROID_MANIFEST); + + if (manifestResource == null + || manifestResource.exists() == false) { + // mark project and exit + String msg = String.format(Messages.s_File_Missing, + AndroidConstants.FN_ANDROID_MANIFEST); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return referencedProjects; + } + + // get the resource folder + IFolder resFolder = project.getFolder( + AndroidConstants.WS_RESOURCES); + + // and the assets folder + IFolder assetsFolder = project.getFolder( + AndroidConstants.WS_ASSETS); + + // we need to make sure this one exists. + if (assetsFolder.exists() == false) { + assetsFolder = null; + } + + IPath resLocation = resFolder.getLocation(); + IPath manifestLocation = manifestResource.getLocation(); + + if (resLocation != null && manifestLocation != null) { + String osResPath = resLocation.toOSString(); + String osManifestPath = manifestLocation.toOSString(); + + String osAssetsPath = null; + if (assetsFolder != null) { + osAssetsPath = assetsFolder.getLocation().toOSString(); + } + + // build the default resource package + if (executeAapt(project, osManifestPath, osResPath, + osAssetsPath, + osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_, + null /*configFilter*/) == false) { + // aapt failed. Whatever files that needed to be marked + // have already been marked. We just return. + return referencedProjects; + } + + // now do the same thing for all the configured resource packages. + if (apkfilters != null) { + for (Entry entry : apkfilters) { + String outPathFormat = osBinPath + File.separator + + AndroidConstants.FN_RESOURCES_S_AP_; + String outPath = String.format(outPathFormat, entry.getKey()); + if (executeAapt(project, osManifestPath, osResPath, + osAssetsPath, outPath, entry.getValue()) == false) { + // aapt failed. Whatever files that needed to be marked + // have already been marked. We just return. + return referencedProjects; + } + } + } + + // build has been done. reset the state of the builder + mPackageResources = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources); + } + } + + // then we check if we need to package the .class into classes.dex + if (mConvertToDex) { + if (executeDx(javaProject, osBinPath, osBinPath + File.separator + + AndroidConstants.FN_CLASSES_DEX, referencedJavaProjects) == false) { + // dx failed, we return + return referencedProjects; + } + + // build has been done. reset the state of the builder + mConvertToDex = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex); + } + + // now we need to make the final package from the intermediary apk + // and classes.dex. + // This is the default package with all the resources. + + String classesDexPath = osBinPath + File.separator + + AndroidConstants.FN_CLASSES_DEX; + if (finalPackage(osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_, + classesDexPath,osFinalPackagePath, javaProject, + referencedJavaProjects) == false) { + return referencedProjects; + } + + // now do the same thing for all the configured resource packages. + if (apkfilters != null) { + String resPathFormat = osBinPath + File.separator + + AndroidConstants.FN_RESOURCES_S_AP_; + + for (Entry entry : apkfilters) { + // make the filename for the resource package. + String resPath = String.format(resPathFormat, entry.getKey()); + + // make the filename for the apk to generate + String apkOsFilePath = osBinPath + File.separator + + ProjectHelper.getApkFilename(project, entry.getKey()); + if (finalPackage(resPath, classesDexPath, apkOsFilePath, javaProject, + referencedJavaProjects) == false) { + return referencedProjects; + } + } + } + + // we are done. + + // get the resource to bin + outputFolder.refreshLocal(IResource.DEPTH_ONE, monitor); + + // build has been done. reset the state of the builder + mBuildFinalPackage = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); + + // reset the installation manager to force new installs of this project + ApkInstallManager.getInstance().resetInstallationFor(project); + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + "Build Success!"); + } + } catch (Exception exception) { + // try to catch other exception to actually display an error. This will be useful + // if we get an NPE or something so that we can at least notify the user that something + // went wrong. + + // first check if this is a CoreException we threw to cancel the build. + if (exception instanceof CoreException) { + if (((CoreException)exception).getStatus().getSeverity() == IStatus.CANCEL) { + // Project is already marked with an error. Nothing to do + return referencedProjects; + } + } + + String msg = exception.getMessage(); + if (msg == null) { + msg = exception.getClass().getCanonicalName(); + } + + msg = String.format("Unknown error: %1$s", msg); + AdtPlugin.printErrorToConsole(project, msg); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + } + + return referencedProjects; + } + + @Override + protected void startupOnInitialize() { + super.startupOnInitialize(); + + // load the build status. We pass true as the default value to + // force a recompile in case the property was not found + mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , true); + mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true); + mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true); + } + + /** + * Executes aapt. If any error happen, files or the project will be marked. + * @param project The Project + * @param osManifestPath The path to the manifest file + * @param osResPath The path to the res folder + * @param osAssetsPath The path to the assets folder. This can be null. + * @param osOutFilePath The path to the temporary resource file to create. + * @param configFilter The configuration filter for the resources to include + * (used with -c option, for example "port,en,fr" to include portrait, English and French + * resources.) + * @return true if success, false otherwise. + */ + private boolean executeAapt(IProject project, String osManifestPath, + String osResPath, String osAssetsPath, String osOutFilePath, String configFilter) { + IAndroidTarget target = Sdk.getCurrent().getTarget(project); + + // Create the command line. + ArrayList commandArray = new ArrayList(); + commandArray.add(target.getPath(IAndroidTarget.AAPT)); + commandArray.add("package"); //$NON-NLS-1$ + commandArray.add("-f");//$NON-NLS-1$ + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + commandArray.add("-v"); //$NON-NLS-1$ + } + if (configFilter != null) { + commandArray.add("-c"); //$NON-NLS-1$ + commandArray.add(configFilter); + } + commandArray.add("-M"); //$NON-NLS-1$ + commandArray.add(osManifestPath); + commandArray.add("-S"); //$NON-NLS-1$ + commandArray.add(osResPath); + if (osAssetsPath != null) { + commandArray.add("-A"); //$NON-NLS-1$ + commandArray.add(osAssetsPath); + } + commandArray.add("-I"); //$NON-NLS-1$ + commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR)); + commandArray.add("-F"); //$NON-NLS-1$ + commandArray.add(osOutFilePath); + + String command[] = commandArray.toArray( + new String[commandArray.size()]); + + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + StringBuilder sb = new StringBuilder(); + for (String c : command) { + sb.append(c); + sb.append(' '); + } + AdtPlugin.printToConsole(project, sb.toString()); + } + + // launch + int execError = 1; + try { + // launch the command line process + Process process = Runtime.getRuntime().exec(command); + + // list to store each line of stderr + ArrayList results = new ArrayList(); + + // get the output and return code from the process + execError = grabProcessOutput(process, results); + + // attempt to parse the error output + boolean parsingError = parseAaptOutput(results, project); + + // if we couldn't parse the output we display it in the console. + if (parsingError) { + if (execError != 0) { + AdtPlugin.printErrorToConsole(project, results.toArray()); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, + results.toArray()); + } + } + + // We need to abort if the exec failed. + if (execError != 0) { + // if the exec failed, and we couldn't parse the error output (and therefore + // not all files that should have been marked, were marked), we put a generic + // marker on the project and abort. + if (parsingError) { + markProject(AndroidConstants.MARKER_PACKAGING, Messages.Unparsed_AAPT_Errors, + IMarker.SEVERITY_ERROR); + } + + // abort if exec failed. + return false; + } + } catch (IOException e1) { + String msg = String.format(Messages.AAPT_Exec_Error, command[0]); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (InterruptedException e) { + String msg = String.format(Messages.AAPT_Exec_Error, command[0]); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return false; + } + + return true; + } + + /** + * Execute the Dx tool for dalvik code conversion. + * @param javaProject The java project + * @param osBinPath the path to the output folder of the project + * @param osOutFilePath the path of the dex file to create. + * @param referencedJavaProjects the list of referenced projects for this project. + * + * @throws CoreException + */ + private boolean executeDx(IJavaProject javaProject, String osBinPath, String osOutFilePath, + IJavaProject[] referencedJavaProjects) throws CoreException { + IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject()); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + if (targetData == null) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); + } + + // get the dex wrapper + DexWrapper wrapper = targetData.getDexWrapper(); + + if (wrapper == null) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); + } + + // make sure dx use the proper output streams. + // first make sure we actually have the streams available. + if (mOutStream == null) { + IProject project = getProject(); + mOutStream = AdtPlugin.getOutPrintStream(project, DX_PREFIX); + mErrStream = AdtPlugin.getErrPrintStream(project, DX_PREFIX); + } + + try { + // get the list of libraries to include with the source code + String[] libraries = getExternalJars(); + + // get the list of referenced projects output to add + String[] projectOutputs = getProjectOutputs(referencedJavaProjects); + + String[] fileNames = new String[1 + projectOutputs.length + libraries.length]; + + // first this project output + fileNames[0] = osBinPath; + + // then other project output + System.arraycopy(projectOutputs, 0, fileNames, 1, projectOutputs.length); + + // then external jars. + System.arraycopy(libraries, 0, fileNames, 1 + projectOutputs.length, libraries.length); + + int res = wrapper.run(osOutFilePath, fileNames, + AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE, + mOutStream, mErrStream); + + if (res != 0) { + // output error message and marker the project. + String message = String.format(Messages.Dalvik_Error_d, + res); + AdtPlugin.printErrorToConsole(getProject(), message); + markProject(AndroidConstants.MARKER_PACKAGING, message, IMarker.SEVERITY_ERROR); + return false; + } + } catch (Throwable ex) { + String message = ex.getMessage(); + if (message == null) { + message = ex.getClass().getCanonicalName(); + } + message = String.format(Messages.Dalvik_Error_s, message); + AdtPlugin.printErrorToConsole(getProject(), message); + markProject(AndroidConstants.MARKER_PACKAGING, message, IMarker.SEVERITY_ERROR); + if ((ex instanceof NoClassDefFoundError) + || (ex instanceof NoSuchMethodError)) { + AdtPlugin.printErrorToConsole(getProject(), Messages.Incompatible_VM_Warning, + Messages.Requires_1_5_Error); + } + return false; + } + + return true; + } + + /** + * Makes the final package. Package the dex files, the temporary resource file into the final + * package file. + * @param intermediateApk The path to the temporary resource file. + * @param dex The path to the dex file. + * @param output The path to the final package file to create. + * @param javaProject + * @param referencedJavaProjects + * @return true if success, false otherwise. + */ + private boolean finalPackage(String intermediateApk, String dex, String output, + final IJavaProject javaProject, IJavaProject[] referencedJavaProjects) { + + FileOutputStream fos = null; + try { + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + String osKeyPath = store.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE); + if (osKeyPath == null || new File(osKeyPath).exists() == false) { + osKeyPath = DebugKeyProvider.getDefaultKeyStoreOsPath(); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + Messages.ApkBuilder_Using_Default_Key); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Using_s_To_Sign, osKeyPath)); + } + + // TODO: get the store type from somewhere else. + DebugKeyProvider provider = new DebugKeyProvider(osKeyPath, null /* storeType */, + new IKeyGenOutput() { + public void err(String message) { + AdtPlugin.printErrorToConsole(javaProject.getProject(), + Messages.ApkBuilder_Signing_Key_Creation_s + message); + } + + public void out(String message) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, + javaProject.getProject(), + Messages.ApkBuilder_Signing_Key_Creation_s + message); + } + }); + PrivateKey key = provider.getDebugKey(); + X509Certificate certificate = (X509Certificate)provider.getCertificate(); + + if (key == null) { + String msg = String.format(Messages.Final_Archive_Error_s, + Messages.ApkBuilder_Unable_To_Gey_Key); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return false; + } + + // compare the certificate expiration date + if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) { + // TODO, regenerate a new one. + String msg = String.format(Messages.Final_Archive_Error_s, + String.format(Messages.ApkBuilder_Certificate_Expired_on_s, + DateFormat.getInstance().format(certificate.getNotAfter()))); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return false; + } + + // create the jar builder. + fos = new FileOutputStream(output); + SignedJarBuilder builder = new SignedJarBuilder(fos, key, certificate); + + // add the intermediate file containing the compiled resources. + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Packaging_s, intermediateApk)); + FileInputStream fis = new FileInputStream(intermediateApk); + try { + builder.writeZip(fis, null /* filter */); + } finally { + fis.close(); + } + + // Now we add the new file to the zip archive for the classes.dex file. + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Packaging_s, + AndroidConstants.FN_CLASSES_DEX)); + File entryFile = new File(dex); + builder.writeFile(entryFile, AndroidConstants.FN_CLASSES_DEX); + + // Now we write the standard resources from the project and the referenced projects. + writeStandardResources(builder, javaProject, referencedJavaProjects); + + // Now we write the standard resources from the external libraries + for (String libraryOsPath : getExternalJars()) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Packaging_s, libraryOsPath)); + try { + fis = new FileInputStream(libraryOsPath); + mResourceFilter.clear(); + builder.writeZip(fis, mResourceFilter); + + // check if we found native libraries in the external library. This + // constitutes an error or warning depending on if they are in lib/ + List nativeLibs = mResourceFilter.getNativeLibs(); + boolean nativeInterference = mResourceFilter.getNativeLibInterefence(); + if (nativeLibs.size() > 0) { + String libName = new File(libraryOsPath).getName(); + String msg = String.format("Native libraries detected in '%1$s'. See console for more information.", + libName); + + + markProject(AndroidConstants.MARKER_PACKAGING, msg, + nativeInterference || + AdtPrefs.getPrefs().getBuildForceErrorOnNativeLibInJar() ? + IMarker.SEVERITY_ERROR : IMarker.SEVERITY_WARNING); + + ArrayList consoleMsgs = new ArrayList(); + consoleMsgs.add(String.format( + "The library '%1$s' contains native libraries that will not run on the device.", + libName)); + if (nativeInterference) { + consoleMsgs.add("Additionally some of those libraries will interfer with the installation of the application because of their location in lib/"); + consoleMsgs.add("lib/ is reserved for NDK libraries."); + } + consoleMsgs.add("The following libraries were found:"); + for (String lib : nativeLibs) { + consoleMsgs.add(" - " + lib); + } + AdtPlugin.printErrorToConsole(javaProject.getProject(), + consoleMsgs.toArray()); + + return false; + } + } finally { + fis.close(); + } + } + + // now write the native libraries. + // First look if the lib folder is there. + IResource libFolder = javaProject.getProject().findMember(SdkConstants.FD_NATIVE_LIBS); + if (libFolder != null && libFolder.exists() && + libFolder.getType() == IResource.FOLDER) { + // look inside and put .so in lib/* by keeping the relative folder path. + writeNativeLibraries((IFolder) libFolder, builder); + } + + // close the jar file and write the manifest and sign it. + builder.close(); + } catch (GeneralSecurityException e1) { + // mark project and return + String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage()); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (IOException e1) { + // mark project and return + String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage()); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (KeytoolException e) { + String eMessage = e.getMessage(); + + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + + // output more info in the console + AdtPlugin.printErrorToConsole(javaProject.getProject(), + msg, + String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()), + Messages.ApkBuilder_Update_or_Execute_manually_s, + e.getCommandLine()); + } catch (AndroidLocationException e) { + String eMessage = e.getMessage(); + + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + + // and also output it in the console + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + } catch (CoreException e) { + // mark project and return + String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage()); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (Exception e) { + // try to catch other exception to actually display an error. This will be useful + // if we get an NPE or something so that we can at least notify the user that something + // went wrong (otherwise the build appears to succeed but the zip archive is not closed + // and therefore invalid. + String msg = e.getMessage(); + if (msg == null) { + msg = e.getClass().getCanonicalName(); + } + + msg = String.format("Unknown error: %1$s", msg); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return false; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + // pass. + } + } + } + + return true; + } + + /** + * Writes native libraries into a {@link SignedJarBuilder}. + *

The native libraries must be located in a given main folder. Under this folder, it is + * expected that the libraries are under a sub-folder that represents the ABI of the library. + * + * The path in the archive is based on the ABI folder name, and located under a main + * folder called "lib". + * + * This method also packages any "gdbserver" executable it finds in the ABI folders. + * + * @param rootFolder The folder containing the native libraries. + * @param jarBuilder the {@link SignedJarBuilder} used to create the archive. + * @throws CoreException + * @throws IOException + */ + private void writeNativeLibraries(IFolder rootFolder, SignedJarBuilder jarBuilder) + throws CoreException, IOException { + // the native files must be under a single sub-folder under the main root folder. + // the sub-folder represents the abi for the native libs + IResource[] abis = rootFolder.members(); + for (IResource abi : abis) { + if (abi.getType() == IResource.FOLDER) { // ignore non folders. + IResource[] libs = ((IFolder)abi).members(); + + for (IResource lib : libs) { + if (lib.getType() == IResource.FILE) { // ignore non files. + IPath path = lib.getFullPath(); + + // check the extension. + String ext = path.getFileExtension(); + if (AndroidConstants.EXT_NATIVE_LIB.equalsIgnoreCase(ext) || + GDBSERVER_NAME.equals(lib.getName())) { + // compute the path inside the archive. + IPath apkPath = new Path(SdkConstants.FD_APK_NATIVE_LIBS); + apkPath = apkPath.append(abi.getName()).append(lib.getName()); + + // writes the file in the apk. + jarBuilder.writeFile(lib.getLocation().toFile(), apkPath.toString()); + } + } + } + } + } + } + + /** + * Writes the standard resources of a project and its referenced projects + * into a {@link SignedJarBuilder}. + * Standard resources are non java/aidl files placed in the java package folders. + * @param jarBuilder the {@link SignedJarBuilder}. + * @param javaProject the javaProject object. + * @param referencedJavaProjects the java projects that this project references. + * @throws IOException + * @throws CoreException + */ + private void writeStandardResources(SignedJarBuilder jarBuilder, IJavaProject javaProject, + IJavaProject[] referencedJavaProjects) throws IOException, CoreException { + IWorkspace ws = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot wsRoot = ws.getRoot(); + + // create a list of path already put into the archive, in order to detect conflict + ArrayList list = new ArrayList(); + + writeStandardProjectResources(jarBuilder, javaProject, wsRoot, list); + + for (IJavaProject referencedJavaProject : referencedJavaProjects) { + // only include output from non android referenced project + // (This is to handle the case of reference Android projects in the context of + // instrumentation projects that need to reference the projects to be tested). + if (referencedJavaProject.getProject().hasNature(AndroidConstants.NATURE) == false) { + writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list); + } + } + } + + /** + * Writes the standard resources of a {@link IJavaProject} into a {@link SignedJarBuilder}. + * Standard resources are non java/aidl files placed in the java package folders. + * @param jarBuilder the {@link SignedJarBuilder}. + * @param javaProject the javaProject object. + * @param wsRoot the {@link IWorkspaceRoot}. + * @param list a list of files already added to the archive, to detect conflicts. + * @throws IOException + */ + private void writeStandardProjectResources(SignedJarBuilder jarBuilder, + IJavaProject javaProject, IWorkspaceRoot wsRoot, ArrayList list) + throws IOException { + // get the source pathes + ArrayList sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject); + + // loop on them and then recursively go through the content looking for matching files. + for (IPath sourcePath : sourceFolders) { + IResource sourceResource = wsRoot.findMember(sourcePath); + if (sourceResource != null && sourceResource.getType() == IResource.FOLDER) { + writeStandardSourceFolderResources(jarBuilder, sourcePath, (IFolder)sourceResource, + list); + } + } + } + + /** + * Recursively writes the standard resources of a source folder into a {@link SignedJarBuilder}. + * Standard resources are non java/aidl files placed in the java package folders. + * @param jarBuilder the {@link SignedJarBuilder}. + * @param sourceFolder the {@link IPath} of the source folder. + * @param currentFolder The current folder we're recursively processing. + * @param list a list of files already added to the archive, to detect conflicts. + * @throws IOException + */ + private void writeStandardSourceFolderResources(SignedJarBuilder jarBuilder, IPath sourceFolder, + IFolder currentFolder, ArrayList list) throws IOException { + try { + IResource[] members = currentFolder.members(); + + for (IResource member : members) { + int type = member.getType(); + if (type == IResource.FILE && member.exists()) { + if (checkFileForPackaging((IFile)member)) { + // this files must be added to the archive. + IPath fullPath = member.getFullPath(); + + // We need to create its path inside the archive. + // This path is relative to the source folder. + IPath relativePath = fullPath.removeFirstSegments( + sourceFolder.segmentCount()); + String zipPath = relativePath.toString(); + + // lets check it's not already in the list of path added to the archive + if (list.indexOf(zipPath) != -1) { + AdtPlugin.printErrorToConsole(getProject(), + String.format( + Messages.ApkBuilder_s_Conflict_with_file_s, + fullPath, zipPath)); + } else { + // get the File object + File entryFile = member.getLocation().toFile(); + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + String.format( + Messages.ApkBuilder_Packaging_s_into_s, + fullPath, zipPath)); + + // write it in the zip archive + jarBuilder.writeFile(entryFile, zipPath); + + // and add it to the list of entries + list.add(zipPath); + } + } + } else if (type == IResource.FOLDER) { + if (checkFolderForPackaging((IFolder)member)) { + writeStandardSourceFolderResources(jarBuilder, sourceFolder, + (IFolder)member, list); + } + } + } + } catch (CoreException e) { + // if we can't get the members of the folder, we just don't do anything. + } + } + + /** + * Returns the list of the output folders for the specified {@link IJavaProject} objects, if + * they are Android projects. + * + * @param referencedJavaProjects the java projects. + * @return an array, always. Can be empty. + * @throws CoreException + */ + private String[] getProjectOutputs(IJavaProject[] referencedJavaProjects) throws CoreException { + ArrayList list = new ArrayList(); + + IWorkspace ws = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot wsRoot = ws.getRoot(); + + for (IJavaProject javaProject : referencedJavaProjects) { + // only include output from non android referenced project + // (This is to handle the case of reference Android projects in the context of + // instrumentation projects that need to reference the projects to be tested). + if (javaProject.getProject().hasNature(AndroidConstants.NATURE) == false) { + // get the output folder + IPath path = null; + try { + path = javaProject.getOutputLocation(); + } catch (JavaModelException e) { + continue; + } + + IResource outputResource = wsRoot.findMember(path); + if (outputResource != null && outputResource.getType() == IResource.FOLDER) { + String outputOsPath = outputResource.getLocation().toOSString(); + + list.add(outputOsPath); + } + } + } + + return list.toArray(new String[list.size()]); + } + + /** + * Returns an array of {@link IJavaProject} matching the provided {@link IProject} objects. + * @param projects the IProject objects. + * @return an array, always. Can be empty. + * @throws CoreException + */ + private IJavaProject[] getJavaProjects(IProject[] projects) throws CoreException { + ArrayList list = new ArrayList(); + + for (IProject p : projects) { + if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) { + + list.add(JavaCore.create(p)); + } + } + + return list.toArray(new IJavaProject[list.size()]); + } + + /** + * Checks a {@link IFile} to make sure it should be packaged as standard resources. + * @param file the IFile representing the file. + * @return true if the file should be packaged as standard java resources. + */ + static boolean checkFileForPackaging(IFile file) { + String name = file.getName(); + + String ext = file.getFileExtension(); + return JavaResourceFilter.checkFileForPackaging(name, ext); + } + + /** + * Checks whether an {@link IFolder} and its content is valid for packaging into the .apk as + * standard Java resource. + * @param folder the {@link IFolder} to check. + */ + static boolean checkFolderForPackaging(IFolder folder) { + String name = folder.getName(); + return JavaResourceFilter.checkFolderForPackaging(name); + } + + @Override + protected void abortOnBadSetup(IJavaProject javaProject) throws CoreException { + super.abortOnBadSetup(javaProject); + + // for this version, we stop on any marker (ie also markers coming from JDT). + // The depth is set to ZERO to make sure we don't stop on warning on resources. + // Only markers set directly on the project are considered. + IMarker[] markers = javaProject.getProject().findMarkers(null /*type*/, + false /*includeSubtypes*/, IResource.DEPTH_ZERO); + + if (markers.length > 0) { + stopBuild(""); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkDeltaVisitor.java new file mode 100644 index 000000000..2cebd91a7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkDeltaVisitor.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.build; + +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.build.BaseBuilder.BaseDeltaVisitor; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; + +import java.util.ArrayList; + +/** + * Delta resource visitor looking for changes that will trigger a new packaging of an Android + * application. + *

+ * This looks for the following changes: + *

    + *
  • Any change to the AndroidManifest.xml file
  • + *
  • Any change inside the assets/ folder
  • + *
  • Any file change inside the res/ folder
  • + *
  • Any .class file change inside the output folder
  • + *
  • Any change to the classes.dex inside the output folder
  • + *
  • Any change to the packaged resources file inside the output folder
  • + *
  • Any change to a non java/aidl file inside the source folders
  • + *
  • Any change to .so file inside the lib (native library) folder
  • + *
+ */ +public class ApkDeltaVisitor extends BaseDeltaVisitor + implements IResourceDeltaVisitor { + + /** + * compile flag. This is set to true if one of the changed/added/removed + * file is a .class file. Upon visiting all the delta resources, if this + * flag is true, then we know we'll have to make the "classes.dex" file. + */ + private boolean mConvertToDex = false; + + /** + * compile flag. This is set to true if one of the changed/added/removed + * file is a resource file. Upon visiting all the delta resources, if + * this flag is true, then we know we'll have to make the intermediate + * apk file. + */ + private boolean mPackageResources = false; + + /** + * Final package flag. This is set to true if one of the changed/added/removed + * file is a non java file (or aidl) in the resource folder. Upon visiting all the + * delta resources, if this flag is true, then we know we'll have to make the final + * package. + */ + private boolean mMakeFinalPackage = false; + + /** List of source folders. */ + private ArrayList mSourceFolders; + + private IPath mOutputPath; + + private IPath mAssetPath; + + private IPath mResPath; + + private IPath mLibFolder; + + /** + * Builds the object with a specified output folder. + * @param builder the xml builder using this object to visit the + * resource delta. + * @param sourceFolders the list of source folders for the project, relative to the workspace. + * @param outputfolder the output folder of the project. + */ + public ApkDeltaVisitor(BaseBuilder builder, ArrayList sourceFolders, + IFolder outputfolder) { + super(builder); + mSourceFolders = sourceFolders; + + if (outputfolder != null) { + mOutputPath = outputfolder.getFullPath(); + } + + IResource assetFolder = builder.getProject().findMember(SdkConstants.FD_ASSETS); + if (assetFolder != null) { + mAssetPath = assetFolder.getFullPath(); + } + + IResource resFolder = builder.getProject().findMember(SdkConstants.FD_RESOURCES); + if (resFolder != null) { + mResPath = resFolder.getFullPath(); + } + + IResource libFolder = builder.getProject().findMember(SdkConstants.FD_NATIVE_LIBS); + if (libFolder != null) { + mLibFolder = libFolder.getFullPath(); + } + } + + public boolean getConvertToDex() { + return mConvertToDex; + } + + public boolean getPackageResources() { + return mPackageResources; + } + + public boolean getMakeFinalPackage() { + return mMakeFinalPackage; + } + + /** + * {@inheritDoc} + * @throws CoreException + * + * @see org.eclipse.core.resources.IResourceDeltaVisitor + * #visit(org.eclipse.core.resources.IResourceDelta) + */ + public boolean visit(IResourceDelta delta) throws CoreException { + // if all flags are true, we can stop going through the resource delta. + if (mConvertToDex && mPackageResources && mMakeFinalPackage) { + return false; + } + + // we are only going to look for changes in res/, src/ and in + // AndroidManifest.xml since the delta visitor goes through the main + // folder before its childre we can check when the path segment + // count is 2 (format will be /$Project/folder) and make sure we are + // processing res/, src/ or AndroidManifest.xml + IResource resource = delta.getResource(); + IPath path = resource.getFullPath(); + String[] pathSegments = path.segments(); + int type = resource.getType(); + + // since the delta visitor also visits the root we return true if + // segments.length = 1 + if (pathSegments.length == 1) { + return true; + } + + // check the manifest. + if (pathSegments.length == 2 && + AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(pathSegments[1])) { + // if the manifest changed we have to repackage the + // resources. + mPackageResources = true; + mMakeFinalPackage = true; + + // we don't want to go to the children, not like they are + // any for this resource anyway. + return false; + } + + // check the other folders. + if (mOutputPath != null && mOutputPath.isPrefixOf(path)) { + // a resource changed inside the output folder. + if (type == IResource.FILE) { + // just check this is a .class file. Any modification will + // trigger a change in the classes.dex file + String ext = resource.getFileExtension(); + if (AndroidConstants.EXT_CLASS.equalsIgnoreCase(ext)) { + mConvertToDex = true; + mMakeFinalPackage = true; + + // no need to check the children, as we are in a package + // and there can only be subpackage children containing + // only .class files + return false; + } + + // check for a few files directly in the output folder and force + // rebuild if they have been deleted. + if (delta.getKind() == IResourceDelta.REMOVED) { + IPath parentPath = path.removeLastSegments(1); + if (mOutputPath.equals(parentPath)) { + String resourceName = resource.getName(); + // check if classes.dex was removed + if (resourceName.equalsIgnoreCase(AndroidConstants.FN_CLASSES_DEX)) { + mConvertToDex = true; + mMakeFinalPackage = true; + } else if (resourceName.equalsIgnoreCase( + AndroidConstants.FN_RESOURCES_AP_) || + AndroidConstants.PATTERN_RESOURCES_S_AP_.matcher( + resourceName).matches()) { + // or if the default resources.ap_ or a configured version + // (resources-###.ap_) was removed. + mPackageResources = true; + mMakeFinalPackage = true; + } + } + } + } + + // if this is a folder, we only go visit it if we don't already know + // that we need to convert to dex already. + return mConvertToDex == false; + } else if (mResPath != null && mResPath.isPrefixOf(path)) { + // in the res folder we are looking for any file modification + // (we don't care about folder being added/removed, only content + // is important) + if (type == IResource.FILE) { + mPackageResources = true; + mMakeFinalPackage = true; + return false; + } + + // for folders, return true only if we don't already know we have to + // package the resources. + return mPackageResources == false; + } else if (mAssetPath != null && mAssetPath.isPrefixOf(path)) { + // this is the assets folder that was modified. + // we don't care what content was changed. All we care + // about is that something changed inside. No need to visit + // the children even. + mPackageResources = true; + mMakeFinalPackage = true; + return false; + } else if (mLibFolder != null && mLibFolder.isPrefixOf(path)) { + // inside the native library folder. Test if the changed resource is a .so file. + if (type == IResource.FILE && + path.getFileExtension().equalsIgnoreCase(AndroidConstants.EXT_NATIVE_LIB)) { + mMakeFinalPackage = true; + return false; // return false for file. + } + + // for folders, return true only if we don't already know we have to make the + // final package. + return mMakeFinalPackage == false; + } else { + // we are in a folder that is neither the resource folders, nor the output. + // check against all the source folders, unless we already know we need to do + // the final package. + // This could be a source folder or a folder leading to a source folder. + // However we only check this if we don't already know that we need to build the + // package anyway + if (mMakeFinalPackage == false) { + for (IPath sourcePath : mSourceFolders) { + if (sourcePath.isPrefixOf(path)) { + // In the source folders, we are looking for any kind of + // modification related to file that are not java files. + // Also excluded are aidl files, and package.html files + if (type == IResource.FOLDER) { + // always visit the subfolders, unless the folder is not to be included + return ApkBuilder.checkFolderForPackaging((IFolder)resource); + } else if (type == IResource.FILE) { + if (ApkBuilder.checkFileForPackaging((IFile)resource)) { + mMakeFinalPackage = true; + } + + return false; + } + + } + } + } + } + + // if the folder is not inside one of the folders we are interested in (res, assets, output, + // source folders), it could be a folder leading to them, so we return true. + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BaseBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BaseBuilder.java new file mode 100644 index 000000000..fde293b59 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BaseBuilder.java @@ -0,0 +1,956 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.build; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler; +import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.XmlErrorListener; +import com.android.ide.eclipse.adt.internal.sdk.LoadStatus; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.xml.sax.SAXException; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Base builder for XML files. This class allows for basic XML parsing with + * error checking and marking the files for errors/warnings. + */ +abstract class BaseBuilder extends IncrementalProjectBuilder { + + // TODO: rename the pattern to something that makes sense + javadoc comments. + + /** + * Single line aapt warning for skipping files.
+ * " (skipping hidden file '<file path>'" + */ + private final static Pattern sPattern0Line1 = Pattern.compile( + "^\\s+\\(skipping hidden file\\s'(.*)'\\)$"); //$NON-NLS-1$ + + /** + * First line of dual line aapt error.
+ * "ERROR at line <line>: <error>"
+ * " (Occurred while parsing <path>)" + */ + private final static Pattern sPattern1Line1 = Pattern.compile( + "^ERROR\\s+at\\s+line\\s+(\\d+):\\s+(.*)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.
+ * "ERROR at line <line>: <error>"
+ * " (Occurred while parsing <path>)"
+ * @see #sPattern1Line1 + */ + private final static Pattern sPattern1Line2 = Pattern.compile( + "^\\s+\\(Occurred while parsing\\s+(.*)\\)$"); //$NON-NLS-1$ + /** + * First line of dual line aapt error.
+ * "ERROR: <error>"
+ * "Defined at file <path> line <line>" + */ + private final static Pattern sPattern2Line1 = Pattern.compile( + "^ERROR:\\s+(.+)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.
+ * "ERROR: <error>"
+ * "Defined at file <path> line <line>"
+ * @see #sPattern2Line1 + */ + private final static Pattern sPattern2Line2 = Pattern.compile( + "Defined\\s+at\\s+file\\s+(.+)\\s+line\\s+(\\d+)"); //$NON-NLS-1$ + /** + * Single line aapt error
+ * "<path> line <line>: <error>" + */ + private final static Pattern sPattern3Line1 = Pattern.compile( + "^(.+)\\sline\\s(\\d+):\\s(.+)$"); //$NON-NLS-1$ + /** + * First line of dual line aapt error.
+ * "ERROR parsing XML file <path>"
+ * "<error> at line <line>" + */ + private final static Pattern sPattern4Line1 = Pattern.compile( + "^Error\\s+parsing\\s+XML\\s+file\\s(.+)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.
+ * "ERROR parsing XML file <path>"
+ * "<error> at line <line>"
+ * @see #sPattern4Line1 + */ + private final static Pattern sPattern4Line2 = Pattern.compile( + "^(.+)\\s+at\\s+line\\s+(\\d+)$"); //$NON-NLS-1$ + + /** + * Single line aapt warning
+ * "<path>:<line>: <error>" + */ + private final static Pattern sPattern5Line1 = Pattern.compile( + "^(.+?):(\\d+):\\s+WARNING:(.+)$"); //$NON-NLS-1$ + + /** + * Single line aapt error
+ * "<path>:<line>: <error>" + */ + private final static Pattern sPattern6Line1 = Pattern.compile( + "^(.+?):(\\d+):\\s+(.+)$"); //$NON-NLS-1$ + + /** + * 4 line aapt error
+ * "ERROR: 9-path image <path> malformed"
+ * Line 2 and 3 are taken as-is while line 4 is ignored (it repeats with
+ * 'ERROR: failure processing <path>) + */ + private final static Pattern sPattern7Line1 = Pattern.compile( + "^ERROR:\\s+9-patch\\s+image\\s+(.+)\\s+malformed\\.$"); //$NON-NLS-1$ + + private final static Pattern sPattern8Line1 = Pattern.compile( + "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$ + + /** + * 2 line aapt error
+ * "ERROR: Invalid configuration: foo"
+ * " ^^^"
+ * There's no need to parse the 2nd line. + */ + private final static Pattern sPattern9Line1 = Pattern.compile( + "^Invalid configuration: (.+)$"); //$NON-NLS-1$ + + /** SAX Parser factory. */ + private SAXParserFactory mParserFactory; + + /** + * Base Resource Delta Visitor to handle XML error + */ + protected static class BaseDeltaVisitor implements XmlErrorListener { + + /** The Xml builder used to validate XML correctness. */ + protected BaseBuilder mBuilder; + + /** + * XML error flag. if true, we keep parsing the ResourceDelta but the + * compilation will not happen (we're putting markers) + */ + public boolean mXmlError = false; + + public BaseDeltaVisitor(BaseBuilder builder) { + mBuilder = builder; + } + + /** + * Finds a matching Source folder for the current path. This checkds if the current path + * leads to, or is a source folder. + * @param sourceFolders The list of source folders + * @param pathSegments The segments of the current path + * @return The segments of the source folder, or null if no match was found + */ + protected static String[] findMatchingSourceFolder(ArrayList sourceFolders, + String[] pathSegments) { + + for (IPath p : sourceFolders) { + // check if we are inside one of those source class path + + // get the segments + String[] srcSegments = p.segments(); + + // compare segments. We want the path of the resource + // we're visiting to be + boolean valid = true; + int segmentCount = pathSegments.length; + + for (int i = 0 ; i < segmentCount; i++) { + String s1 = pathSegments[i]; + String s2 = srcSegments[i]; + + if (s1.equalsIgnoreCase(s2) == false) { + valid = false; + break; + } + } + + if (valid) { + // this folder, or one of this children is a source + // folder! + // we return its segments + return srcSegments; + } + } + + return null; + } + + /** + * Sent when an XML error is detected. + * @see XmlErrorListener + */ + public void errorFound() { + mXmlError = true; + } + } + + public BaseBuilder() { + super(); + mParserFactory = SAXParserFactory.newInstance(); + + // FIXME when the compiled XML support for namespace is in, set this to true. + mParserFactory.setNamespaceAware(false); + } + + /** + * Checks an Xml file for validity. Errors/warnings will be marked on the + * file + * @param resource the resource to check + * @param visitor a valid resource delta visitor + */ + protected final void checkXML(IResource resource, BaseDeltaVisitor visitor) { + + // first make sure this is an xml file + if (resource instanceof IFile) { + IFile file = (IFile)resource; + + // remove previous markers + removeMarkersFromFile(file, AndroidConstants.MARKER_XML); + + // create the error handler + XmlErrorHandler reporter = new XmlErrorHandler(file, visitor); + try { + // parse + getParser().parse(file.getContents(), reporter); + } catch (Exception e1) { + } + } + } + + /** + * Returns the SAXParserFactory, instantiating it first if it's not already + * created. + * @return the SAXParserFactory object + * @throws ParserConfigurationException + * @throws SAXException + */ + protected final SAXParser getParser() throws ParserConfigurationException, + SAXException { + return mParserFactory.newSAXParser(); + } + + /** + * Adds a marker to the current project. This methods catches thrown {@link CoreException}, + * and returns null instead. + * + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param severity the severity of the marker. + * @return the marker that was created (or null if failure) + * @see IMarker + */ + protected final IMarker markProject(String markerId, String message, int severity) { + return BaseProjectHelper.markResource(getProject(), markerId, message, severity); + } + + /** + * Removes markers from a file. + * @param file The file from which to delete the markers. + * @param markerId The id of the markers to remove. If null, all marker of + * type IMarker.PROBLEM will be removed. + */ + protected final void removeMarkersFromFile(IFile file, String markerId) { + try { + if (file.exists()) { + file.deleteMarkers(markerId, true, IResource.DEPTH_ZERO); + } + } catch (CoreException ce) { + String msg = String.format(Messages.Marker_Delete_Error, markerId, file.toString()); + AdtPlugin.printErrorToConsole(getProject(), msg); + } + } + + /** + * Removes markers from a container and its children. + * @param folder The container from which to delete the markers. + * @param markerId The id of the markers to remove. If null, all marker of + * type IMarker.PROBLEM will be removed. + */ + protected final void removeMarkersFromContainer(IContainer folder, String markerId) { + try { + if (folder.exists()) { + folder.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE); + } + } catch (CoreException ce) { + String msg = String.format(Messages.Marker_Delete_Error, markerId, folder.toString()); + AdtPlugin.printErrorToConsole(getProject(), msg); + } + } + + /** + * Removes markers from a project and its children. + * @param project The project from which to delete the markers + * @param markerId The id of the markers to remove. If null, all marker of + * type IMarker.PROBLEM will be removed. + */ + protected final static void removeMarkersFromProject(IProject project, + String markerId) { + try { + if (project.exists()) { + project.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE); + } + } catch (CoreException ce) { + String msg = String.format(Messages.Marker_Delete_Error, markerId, project.getName()); + AdtPlugin.printErrorToConsole(project, msg); + } + } + + /** + * Get the stderr output of a process and return when the process is done. + * @param process The process to get the ouput from + * @param results The array to store the stderr output + * @return the process return code. + * @throws InterruptedException + */ + protected final int grabProcessOutput(final Process process, + final ArrayList results) + throws InterruptedException { + // Due to the limited buffer size on windows for the standard io (stderr, stdout), we + // *need* to read both stdout and stderr all the time. If we don't and a process output + // a large amount, this could deadlock the process. + + // read the lines as they come. if null is returned, it's + // because the process finished + new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + results.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }.start(); + + new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + IProject project = getProject(); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, + project, line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + + }.start(); + + // get the return code from the process + return process.waitFor(); + } + + /** + * Parse the output of aapt and mark the incorrect file with error markers + * + * @param results the output of aapt + * @param project the project containing the file to mark + * @return true if the parsing failed, false if success. + */ + protected final boolean parseAaptOutput(ArrayList results, + IProject project) { + // nothing to parse? just return false; + if (results.size() == 0) { + return false; + } + + // get the root of the project so that we can make IFile from full + // file path + String osRoot = project.getLocation().toOSString(); + + Matcher m; + + for (int i = 0; i < results.size(); i++) { + String p = results.get(i); + + m = sPattern0Line1.matcher(p); + if (m.matches()) { + // we ignore those (as this is an ignore message from aapt) + continue; + } + + m = sPattern1Line1.matcher(p); + if (m.matches()) { + String lineStr = m.group(1); + String msg = m.group(2); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern1Line2); + if (m == null) { + return true; + } + + String location = m.group(1); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + continue; + } + + // this needs to be tested before Pattern2 since they both start with 'ERROR:' + m = sPattern7Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String msg = p; // default msg is the line in case we don't find anything else + + if (++i < results.size()) { + msg = results.get(i).trim(); + if (++i < results.size()) { + msg = msg + " - " + results.get(i).trim(); //$NON-NLS-1$ + + // skip the next line + i++; + } + } + + // display the error + if (checkAndMark(location, null, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern2Line1.matcher(p); + if (m.matches()) { + // get the msg + String msg = m.group(1); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern2Line2); + if (m == null) { + return true; + } + + String location = m.group(1); + String lineStr = m.group(2); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + continue; + } + + m = sPattern3Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern4Line1.matcher(p); + if (m.matches()) { + // get the filename. + String location = m.group(1); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern4Line2); + if (m == null) { + return true; + } + + String msg = m.group(1); + String lineStr = m.group(2); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern5Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern6Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern8Line1.matcher(p); + if (m.matches()) { + String location = m.group(2); + String msg = m.group(1); + + // check the values and attempt to mark the file. + if (checkAndMark(location, null, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern9Line1.matcher(p); + if (m.matches()) { + String badConfig = m.group(1); + String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig); + + // skip the next line + i++; + + // check the values and attempt to mark the file. + if (checkAndMark(null /*location*/, null, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + // invalid line format, flag as error, and bail + return true; + } + + return false; + } + + + + /** + * Saves a String property into the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + protected boolean saveProjectStringProperty(String propertyName, String value) { + IProject project = getProject(); + return ProjectHelper.saveStringProperty(project, propertyName, value); + } + + + /** + * Loads a String property from the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return the property value or null if it was not found. + */ + protected String loadProjectStringProperty(String propertyName) { + IProject project = getProject(); + return ProjectHelper.loadStringProperty(project, propertyName); + } + + /** + * Saves a property into the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + protected boolean saveProjectBooleanProperty(String propertyName, boolean value) { + IProject project = getProject(); + return ProjectHelper.saveStringProperty(project, propertyName, Boolean.toString(value)); + } + + /** + * Loads a boolean property from the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param defaultValue The default value to return if the property was not found. + * @return the property value or the default value if the property was not found. + */ + protected boolean loadProjectBooleanProperty(String propertyName, boolean defaultValue) { + IProject project = getProject(); + return ProjectHelper.loadBooleanProperty(project, propertyName, defaultValue); + } + + /** + * Saves the path of a resource into the persistent storate of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param resource the resource which path is saved. + * @return true if the save succeeded + */ + protected boolean saveProjectResourceProperty(String propertyName, IResource resource) { + return ProjectHelper.saveResourceProperty(getProject(), propertyName, resource); + } + + /** + * Loads the path of a resource from the persistent storage of the project, and returns the + * corresponding IResource object. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return The corresponding IResource object (or children interface) or null + */ + protected IResource loadProjectResourceProperty(String propertyName) { + IProject project = getProject(); + return ProjectHelper.loadResourceProperty(project, propertyName); + } + + /** + * Check if the parameters gotten from the error output are valid, and mark + * the file with an AAPT marker. + * @param location the full OS path of the error file. If null, the project is marked + * @param lineStr + * @param message + * @param root The root directory of the project, in OS specific format. + * @param project + * @param markerId The marker id to put. + * @param severity The severity of the marker to put (IMarker.SEVERITY_*) + * @return true if the parameters were valid and the file was marked successfully. + * + * @see IMarker + */ + private final boolean checkAndMark(String location, String lineStr, + String message, String root, IProject project, String markerId, int severity) { + // check this is in fact a file + if (location != null) { + File f = new File(location); + if (f.exists() == false) { + return false; + } + } + + // get the line number + int line = -1; // default value for error with no line. + + if (lineStr != null) { + try { + line = Integer.parseInt(lineStr); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid + // file number. Parsing failed and we return true + return false; + } + } + + // add the marker + IResource f2 = project; + if (location != null) { + f2 = getResourceFromFullPath(location, root, project); + if (f2 == null) { + return false; + } + } + + // check if there's a similar marker already, since aapt is launched twice + boolean markerAlreadyExists = false; + try { + IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO); + + for (IMarker marker : markers) { + int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); + if (tmpLine != line) { + break; + } + + int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1); + if (tmpSeverity != severity) { + break; + } + + String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); + if (tmpMsg == null || tmpMsg.equals(message) == false) { + break; + } + + // if we're here, all the marker attributes are equals, we found it + // and exit + markerAlreadyExists = true; + break; + } + + } catch (CoreException e) { + // if we couldn't get the markers, then we just mark the file again + // (since markerAlreadyExists is initialized to false, we do nothing) + } + + if (markerAlreadyExists == false) { + BaseProjectHelper.markResource(f2, markerId, message, line, severity); + } + + return true; + } + + /** + * Returns a matching matcher for the next line + * @param lines The array of lines + * @param nextIndex The index of the next line + * @param pattern The pattern to match + * @return null if error or no match, the matcher otherwise. + */ + private final Matcher getNextLineMatcher(ArrayList lines, + int nextIndex, Pattern pattern) { + // unless we can't, because we reached the last line + if (nextIndex == lines.size()) { + // we expected a 2nd line, so we flag as error + // and we bail + return null; + } + + Matcher m = pattern.matcher(lines.get(nextIndex)); + if (m.matches()) { + return m; + } + + return null; + } + + private IResource getResourceFromFullPath(String filename, String root, + IProject project) { + if (filename.startsWith(root)) { + String file = filename.substring(root.length()); + + // get the resource + IResource r = project.findMember(file); + + // if the resource is valid, we add the marker + if (r.exists()) { + return r; + } + } + + return null; + } + + /** + * Returns an array of external jar files used by the project. + * @return an array of OS-specific absolute file paths + */ + protected final String[] getExternalJars() { + // get the current project + IProject project = getProject(); + + // get a java project from it + IJavaProject javaProject = JavaCore.create(project); + + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + + ArrayList oslibraryList = new ArrayList(); + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY || + e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + // if this is a classpath variable reference, we resolve it. + if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + e = JavaCore.getResolvedClasspathEntry(e); + } + + // get the IPath + IPath path = e.getPath(); + + // check the name ends with .jar + if (AndroidConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { + boolean local = false; + IResource resource = wsRoot.findMember(path); + if (resource != null && resource.exists() && + resource.getType() == IResource.FILE) { + local = true; + oslibraryList.add(resource.getLocation().toOSString()); + } + + if (local == false) { + // if the jar path doesn't match a workspace resource, + // then we get an OSString and check if this links to a valid file. + String osFullPath = path.toOSString(); + + File f = new File(osFullPath); + if (f.exists()) { + oslibraryList.add(osFullPath); + } else { + String message = String.format( Messages.Couldnt_Locate_s_Error, + path); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, + project, message); + + // Also put a warning marker on the project + markProject(AndroidConstants.MARKER_PACKAGING, message, + IMarker.SEVERITY_WARNING); + } + } + } + } + } + } + + return oslibraryList.toArray(new String[oslibraryList.size()]); + } + + /** + * Aborts the build if the SDK/project setups are broken. This does not + * display any errors. + * + * @param javaProject The {@link IJavaProject} being compiled. + * @throws CoreException + */ + protected void abortOnBadSetup(IJavaProject javaProject) throws CoreException { + IProject iProject = javaProject.getProject(); + // check if we have finished loading the project target. + Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + // we exit silently + stopBuild("SDK is not loaded yet"); + } + + // get the target for the project + IAndroidTarget target = sdk.getTarget(javaProject.getProject()); + + if (target == null) { + // we exit silently + stopBuild("Project target not resolved yet."); + } + + // check on the target data. + if (sdk.checkAndLoadTargetData(target, javaProject) != LoadStatus.LOADED) { + // we exit silently + stopBuild("Project target not loaded yet."); + } + + // abort if there are TARGET or ADT type markers + IMarker[] markers = iProject.findMarkers(AndroidConstants.MARKER_TARGET, + false /*includeSubtypes*/, IResource.DEPTH_ZERO); + + if (markers.length > 0) { + stopBuild(""); + } + + markers = iProject.findMarkers(AndroidConstants.MARKER_ADT, false /*includeSubtypes*/, + IResource.DEPTH_ZERO); + + if (markers.length > 0) { + stopBuild(""); + } + } + + /** + * Throws an exception to cancel the build. + * + * @param error the error message + * @param args the printf-style arguments to the error message. + * @throws CoreException + */ + protected final void stopBuild(String error, Object... args) throws CoreException { + throw new CoreException(new Status(IStatus.CANCEL, AdtPlugin.PLUGIN_ID, + String.format(error, args))); + } + + /** + * Recursively delete all the derived resources. + */ + protected void removeDerivedResources(IResource resource, IProgressMonitor monitor) + throws CoreException { + if (resource.exists()) { + if (resource.isDerived()) { + resource.delete(true, new SubProgressMonitor(monitor, 10)); + } else if (resource.getType() == IResource.FOLDER) { + IFolder folder = (IFolder)resource; + IResource[] members = folder.members(); + for (IResource member : members) { + removeDerivedResources(member, monitor); + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java new file mode 100644 index 000000000..c1698da73 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java @@ -0,0 +1,137 @@ + +package com.android.ide.eclipse.adt.internal.build; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.internal.build.build_messages"; //$NON-NLS-1$ + + public static String AAPT_Error; + + public static String AAPT_Exec_Error; + + public static String Added_s_s_Needs_Updating; + + public static String AIDL_Exec_Error; + + public static String AIDL_Java_Conflict; + + public static String ApkBuilder_Certificate_Expired_on_s; + + public static String ApkBuilder_JAVA_HOME_is_s; + + public static String ApkBuilder_Packaging_s; + + public static String ApkBuilder_Packaging_s_into_s; + + public static String ApkBuilder_s_Conflict_with_file_s; + + public static String ApkBuilder_Signing_Key_Creation_s; + + public static String ApkBuilder_Unable_To_Gey_Key; + + public static String ApkBuilder_UnableBuild_Dex_Not_loaded; + + public static String ApkBuilder_Update_or_Execute_manually_s; + + public static String ApkBuilder_Using_Default_Key; + + public static String ApkBuilder_Using_s_To_Sign; + + public static String Checking_Package_Change; + + public static String Compiler_Compliance_Error; + + public static String Couldnt_Locate_s_Error; + + public static String Dalvik_Error_d; + + public static String Dalvik_Error_s; + + public static String Delete_Obsolete_Error; + + public static String DexWrapper_Dex_Loader; + + public static String DexWrapper_Failed_to_load_s; + + public static String DexWrapper_s_does_not_exists; + + public static String DexWrapper_SecuryEx_Unable_To_Find_API; + + public static String DexWrapper_SecuryEx_Unable_To_Find_Field; + + public static String DexWrapper_SecuryEx_Unable_To_Find_Method; + + public static String DexWrapper_Unable_To_Execute_Dex_s; + + public static String DX_Jar_Error; + + public static String Failed_To_Get_Output; + + public static String Final_Archive_Error_s; + + public static String Incompatible_VM_Warning; + + public static String Marker_Delete_Error; + + public static String No_SDK_Setup_Error; + + public static String Nothing_To_Compile; + + public static String Output_Missing; + + public static String Package_s_Doesnt_Exist_Error; + + public static String Preparing_Generated_Files; + + public static String Project_Has_Errors; + + public static String Refreshing_Res; + + public static String Removing_Generated_Classes; + + public static String Requires_1_5_Error; + + public static String Requires_Class_Compatibility_5; + + public static String Requires_Compiler_Compliance_5; + + public static String Requires_Source_Compatibility_5; + + public static String s_Contains_Xml_Error; + + public static String s_Doesnt_Declare_Package_Error; + + public static String s_File_Missing; + + public static String s_Missing_Repackaging; + + public static String s_Modified_Manually_Recreating_s; + + public static String s_Modified_Recreating_s; + + public static String s_Removed_Recreating_s; + + public static String s_Removed_s_Needs_Updating; + + public static String Start_Full_Apk_Build; + + public static String Start_Full_Pre_Compiler; + + public static String Start_Inc_Apk_Build; + + public static String Start_Inc_Pre_Compiler; + + public static String Unparsed_AAPT_Errors; + + public static String Unparsed_AIDL_Errors; + + public static String Xml_Error; + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerBuilder.java new file mode 100644 index 000000000..ee85e1bc3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerBuilder.java @@ -0,0 +1,1084 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.build; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestParser; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.FixLaunchConfig; +import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.BasicXmlErrorListener; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.xml.AndroidManifest; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Pre Java Compiler. + * This incremental builder performs 2 tasks: + *
    + *
  • compiles the resources located in the res/ folder, along with the + * AndroidManifest.xml file into the R.java class.
  • + *
  • compiles any .aidl files into a corresponding java file.
  • + *
+ * + */ +public class PreCompilerBuilder extends BaseBuilder { + + public static final String ID = "com.android.ide.eclipse.adt.PreCompilerBuilder"; //$NON-NLS-1$ + + private static final String PROPERTY_PACKAGE = "manifestPackage"; //$NON-NLS-1$ + + private static final String PROPERTY_COMPILE_RESOURCES = "compileResources"; //$NON-NLS-1$ + private static final String PROPERTY_COMPILE_AIDL = "compileAidl"; //$NON-NLS-1$ + + /** + * Single line aidl error
+ * "<path>:<line>: <error>" + * or + * "<path>:<line> <error>" + */ + private static Pattern sAidlPattern1 = Pattern.compile("^(.+?):(\\d+):?\\s(.+)$"); //$NON-NLS-1$ + + /** + * Data to temporarly store aidl source file information + */ + static class AidlData { + IFile aidlFile; + IFolder sourceFolder; + + AidlData(IFolder sourceFolder, IFile aidlFile) { + this.sourceFolder = sourceFolder; + this.aidlFile = aidlFile; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj instanceof AidlData) { + AidlData file = (AidlData)obj; + return aidlFile.equals(file.aidlFile) && sourceFolder.equals(file.sourceFolder); + } + + return false; + } + } + + /** + * Resource Compile flag. This flag is reset to false after each successful compilation, and + * stored in the project persistent properties. This allows the builder to remember its state + * when the project is closed/opened. + */ + private boolean mMustCompileResources = false; + + /** List of .aidl files found that are modified or new. */ + private final ArrayList mAidlToCompile = new ArrayList(); + + /** List of .aidl files that have been removed. */ + private final ArrayList mAidlToRemove = new ArrayList(); + + /** cache of the java package defined in the manifest */ + private String mManifestPackage; + + /** Output folder for generated Java File. Created on the Builder init + * @see #startupOnInitialize() + */ + private IFolder mGenFolder; + + /** + * Progress monitor used at the end of every build to refresh the content of the 'gen' folder + * and set the generated files as derived. + */ + private DerivedProgressMonitor mDerivedProgressMonitor; + + /** + * Progress monitor waiting the end of the process to set a persistent value + * in a file. This is typically used in conjunction with IResource.refresh(), + * since this call is asysnchronous, and we need to wait for it to finish for the file + * to be known by eclipse, before we can call resource.setPersistentProperty on + * a new file. + */ + private static class DerivedProgressMonitor implements IProgressMonitor { + private boolean mCancelled = false; + private final ArrayList mFileList = new ArrayList(); + private boolean mDone = false; + public DerivedProgressMonitor() { + } + + void addFile(IFile file) { + mFileList.add(file); + } + + void reset() { + mFileList.clear(); + mDone = false; + } + + public void beginTask(String name, int totalWork) { + } + + public void done() { + if (mDone == false) { + mDone = true; + for (IFile file : mFileList) { + if (file.exists()) { + try { + file.setDerived(true); + } catch (CoreException e) { + // This really shouldn't happen since we check that the resource exist. + // Worst case scenario, the resource isn't marked as derived. + } + } + } + } + } + + public void internalWorked(double work) { + } + + public boolean isCanceled() { + return mCancelled; + } + + public void setCanceled(boolean value) { + mCancelled = value; + } + + public void setTaskName(String name) { + } + + public void subTask(String name) { + } + + public void worked(int work) { + } + } + + public PreCompilerBuilder() { + super(); + } + + // build() returns a list of project from which this project depends for future compilation. + @SuppressWarnings("unchecked") + @Override + protected IProject[] build(int kind, Map args, IProgressMonitor monitor) + throws CoreException { + try { + mDerivedProgressMonitor.reset(); + + // First thing we do is go through the resource delta to not + // lose it if we have to abort the build for any reason. + + // get the project objects + IProject project = getProject(); + IJavaProject javaProject = JavaCore.create(project); + + // Top level check to make sure the build can move forward. + abortOnBadSetup(javaProject); + + IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); + + // now we need to get the classpath list + ArrayList sourceFolderPathList = BaseProjectHelper.getSourceClasspaths( + javaProject); + + PreCompilerDeltaVisitor dv = null; + String javaPackage = null; + String minSdkVersion = null; + + if (kind == FULL_BUILD) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Full_Pre_Compiler); + mMustCompileResources = true; + buildAidlCompilationList(project, sourceFolderPathList); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Inc_Pre_Compiler); + + // Go through the resources and see if something changed. + // Even if the mCompileResources flag is true from a previously aborted + // build, we need to go through the Resource delta to get a possible + // list of aidl files to compile/remove. + IResourceDelta delta = getDelta(project); + if (delta == null) { + mMustCompileResources = true; + buildAidlCompilationList(project, sourceFolderPathList); + } else { + dv = new PreCompilerDeltaVisitor(this, sourceFolderPathList); + delta.accept(dv); + + // record the state + mMustCompileResources |= dv.getCompileResources(); + + if (dv.getForceAidlCompile()) { + buildAidlCompilationList(project, sourceFolderPathList); + } else { + // handle aidl modification, and update mMustCompileAidl + mergeAidlFileModifications(dv.getAidlToCompile(), + dv.getAidlToRemove()); + } + + // get the java package from the visitor + javaPackage = dv.getManifestPackage(); + minSdkVersion = dv.getMinSdkVersion(); + } + } + + // store the build status in the persistent storage + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mMustCompileResources); + + // if there was some XML errors, we just return w/o doing + // anything since we've put some markers in the files anyway. + if (dv != null && dv.mXmlError) { + AdtPlugin.printErrorToConsole(project, Messages.Xml_Error); + + // This interrupts the build. The next builders will not run. + stopBuild(Messages.Xml_Error); + } + + + // get the manifest file + IFile manifest = AndroidManifestParser.getManifest(project); + + if (manifest == null) { + String msg = String.format(Messages.s_File_Missing, + AndroidConstants.FN_ANDROID_MANIFEST); + AdtPlugin.printErrorToConsole(project, msg); + markProject(AndroidConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + + // TODO: document whether code below that uses manifest (which is now guaranteed + // to be null) will actually be executed or not. + } + + // lets check the XML of the manifest first, if that hasn't been done by the + // resource delta visitor yet. + if (dv == null || dv.getCheckedManifestXml() == false) { + BasicXmlErrorListener errorListener = new BasicXmlErrorListener(); + AndroidManifestParser parser = BaseProjectHelper.parseManifestForError(manifest, + errorListener); + + if (errorListener.mHasXmlError == true) { + // there was an error in the manifest, its file has been marked, + // by the XmlErrorHandler. + // We return; + String msg = String.format(Messages.s_Contains_Xml_Error, + AndroidConstants.FN_ANDROID_MANIFEST); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } + + // get the java package from the parser + javaPackage = parser.getPackage(); + minSdkVersion = parser.getApiLevelRequirement(); + } + + if (minSdkVersion != null) { + int minSdkValue = -1; + try { + minSdkValue = Integer.parseInt(minSdkVersion); + } catch (NumberFormatException e) { + // it's ok, it means minSdkVersion contains a (hopefully) valid codename. + } + + AndroidVersion projectVersion = projectTarget.getVersion(); + + // remove earlier marker from the manifest + removeMarkersFromFile(manifest, AndroidConstants.MARKER_ADT); + + if (minSdkValue != -1) { + String codename = projectVersion.getCodename(); + if (codename != null) { + // integer minSdk when the target is a preview => fatal error + String msg = String.format( + "Platform %1$s is a preview and requires appication manifest to set %2$s to '%1$s'", + codename, AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifest, AndroidConstants.MARKER_ADT, msg, + IMarker.SEVERITY_ERROR); + stopBuild(msg); + } else if (minSdkValue < projectVersion.getApiLevel()) { + // integer minSdk is not high enough for the target => warning + String msg = String.format( + "Attribute %1$s (%2$d) is lower than the project target API level (%3$d)", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, + minSdkValue, projectVersion.getApiLevel()); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg); + BaseProjectHelper.markResource(manifest, AndroidConstants.MARKER_ADT, msg, + IMarker.SEVERITY_WARNING); + } else if (minSdkValue > projectVersion.getApiLevel()) { + // integer minSdk is too high for the target => warning + String msg = String.format( + "Attribute %1$s (%2$d) is higher than the project target API level (%3$d)", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, + minSdkValue, projectVersion.getApiLevel()); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg); + BaseProjectHelper.markResource(manifest, AndroidConstants.MARKER_ADT, msg, + IMarker.SEVERITY_WARNING); + } + } else { + // looks like the min sdk is a codename, check it matches the codename + // of the platform + String codename = projectVersion.getCodename(); + if (codename == null) { + // platform is not a preview => fatal error + String msg = String.format( + "Manifest attribute '%1$s' is set to '%2$s'. Integer is expected.", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, minSdkVersion); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifest, AndroidConstants.MARKER_ADT, msg, + IMarker.SEVERITY_ERROR); + stopBuild(msg); + } else if (codename.equals(minSdkVersion) == false) { + // platform and manifest codenames don't match => fatal error. + String msg = String.format( + "Value of manifest attribute '%1$s' does not match platform codename '%2$s'", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, codename); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifest, AndroidConstants.MARKER_ADT, msg, + IMarker.SEVERITY_ERROR); + stopBuild(msg); + } + } + } else if (projectTarget.getVersion().isPreview()) { + // else the minSdkVersion is not set but we are using a preview target. + // Display an error + String codename = projectTarget.getVersion().getCodename(); + String msg = String.format( + "Platform %1$s is a preview and requires appication manifests to set %2$s to '%1$s'", + codename, AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifest, AndroidConstants.MARKER_ADT, msg, + IMarker.SEVERITY_ERROR); + stopBuild(msg); + } + + if (javaPackage == null || javaPackage.length() == 0) { + // looks like the AndroidManifest file isn't valid. + String msg = String.format(Messages.s_Doesnt_Declare_Package_Error, + AndroidConstants.FN_ANDROID_MANIFEST); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifest, AndroidConstants.MARKER_ADT, + msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + // This also throws an exception and nothing beyond this line will run. + stopBuild(msg); + } else if (javaPackage.indexOf('.') == -1) { + // The application package name does not contain 2+ segments! + String msg = String.format( + "Application package '%1$s' must have a minimum of 2 segments.", + AndroidConstants.FN_ANDROID_MANIFEST); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifest, AndroidConstants.MARKER_ADT, + msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + // This also throws an exception and nothing beyond this line will run. + stopBuild(msg); + } + + // at this point we have the java package. We need to make sure it's not a different + // package than the previous one that were built. + if (javaPackage.equals(mManifestPackage) == false) { + // The manifest package has changed, the user may want to update + // the launch configuration + if (mManifestPackage != null) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Checking_Package_Change); + + FixLaunchConfig flc = new FixLaunchConfig(project, mManifestPackage, + javaPackage); + flc.start(); + } + + // now we delete the generated classes from their previous location + deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS, + mManifestPackage); + deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS, + mManifestPackage); + + // record the new manifest package, and save it. + mManifestPackage = javaPackage; + saveProjectStringProperty(PROPERTY_PACKAGE, mManifestPackage); + } + + if (mMustCompileResources) { + // we need to figure out where to store the R class. + // get the parent folder for R.java and update mManifestPackageSourceFolder + IFolder packageFolder = getGenManifestPackageFolder(project); + + // get the resource folder + IFolder resFolder = project.getFolder(AndroidConstants.WS_RESOURCES); + + // get the file system path + IPath outputLocation = mGenFolder.getLocation(); + IPath resLocation = resFolder.getLocation(); + IPath manifestLocation = manifest == null ? null : manifest.getLocation(); + + // those locations have to exist for us to do something! + if (outputLocation != null && resLocation != null + && manifestLocation != null) { + String osOutputPath = outputLocation.toOSString(); + String osResPath = resLocation.toOSString(); + String osManifestPath = manifestLocation.toOSString(); + + // remove the aapt markers + removeMarkersFromFile(manifest, AndroidConstants.MARKER_AAPT_COMPILE); + removeMarkersFromContainer(resFolder, AndroidConstants.MARKER_AAPT_COMPILE); + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Preparing_Generated_Files); + + // since the R.java file may be already existing in read-only + // mode we need to make it readable so that aapt can overwrite + // it + IFile rJavaFile = packageFolder.getFile(AndroidConstants.FN_RESOURCE_CLASS); + + // do the same for the Manifest.java class + IFile manifestJavaFile = packageFolder.getFile( + AndroidConstants.FN_MANIFEST_CLASS); + + // we actually need to delete the manifest.java as it may become empty and + // in this case aapt doesn't generate an empty one, but instead doesn't + // touch it. + manifestJavaFile.delete(true, null); + + // launch aapt: create the command line + ArrayList array = new ArrayList(); + array.add(projectTarget.getPath(IAndroidTarget.AAPT)); + array.add("package"); //$NON-NLS-1$ + array.add("-m"); //$NON-NLS-1$ + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + array.add("-v"); //$NON-NLS-1$ + } + array.add("-J"); //$NON-NLS-1$ + array.add(osOutputPath); + array.add("-M"); //$NON-NLS-1$ + array.add(osManifestPath); + array.add("-S"); //$NON-NLS-1$ + array.add(osResPath); + array.add("-I"); //$NON-NLS-1$ + array.add(projectTarget.getPath(IAndroidTarget.ANDROID_JAR)); + + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + StringBuilder sb = new StringBuilder(); + for (String c : array) { + sb.append(c); + sb.append(' '); + } + String cmd_line = sb.toString(); + AdtPlugin.printToConsole(project, cmd_line); + } + + // launch + int execError = 1; + try { + // launch the command line process + Process process = Runtime.getRuntime().exec( + array.toArray(new String[array.size()])); + + // list to store each line of stderr + ArrayList results = new ArrayList(); + + // get the output and return code from the process + execError = grabProcessOutput(process, results); + + // attempt to parse the error output + boolean parsingError = parseAaptOutput(results, project); + + // if we couldn't parse the output we display it in the console. + if (parsingError) { + if (execError != 0) { + AdtPlugin.printErrorToConsole(project, results.toArray()); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.NORMAL, + project, results.toArray()); + } + } + + if (execError != 0) { + // if the exec failed, and we couldn't parse the error output + // (and therefore not all files that should have been marked, + // were marked), we put a generic marker on the project and abort. + if (parsingError) { + markProject(AndroidConstants.MARKER_ADT, + Messages.Unparsed_AAPT_Errors, IMarker.SEVERITY_ERROR); + } + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.AAPT_Error); + + // abort if exec failed. + // This interrupts the build. The next builders will not run. + stopBuild(Messages.AAPT_Error); + } + } catch (IOException e1) { + // something happen while executing the process, + // mark the project and exit + String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); + markProject(AndroidConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } catch (InterruptedException e) { + // we got interrupted waiting for the process to end... + // mark the project and exit + String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); + markProject(AndroidConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } + + // if the return code was OK, we refresh the folder that + // contains R.java to force a java recompile. + if (execError == 0) { + // now add the R.java/Manifest.java to the list of file to be marked + // as derived. + mDerivedProgressMonitor.addFile(rJavaFile); + mDerivedProgressMonitor.addFile(manifestJavaFile); + + // build has been done. reset the state of the builder + mMustCompileResources = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, + mMustCompileResources); + } + } + } else { + // nothing to do + } + + // now handle the aidl stuff. + boolean aidlStatus = handleAidl(projectTarget, sourceFolderPathList, monitor); + + if (aidlStatus == false && mMustCompileResources == false) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Nothing_To_Compile); + } + } finally { + // refresh the 'gen' source folder. Once this is done with the custom progress + // monitor to mark all new files as derived + mGenFolder.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor); + } + + return null; + } + + @Override + protected void clean(IProgressMonitor monitor) throws CoreException { + super.clean(monitor); + + // Get the project. + IProject project = getProject(); + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Removing_Generated_Classes); + + // remove all the derived resources from the 'gen' source folder. + removeDerivedResources(mGenFolder, monitor); + + + // Clear the project of the generic markers + removeMarkersFromProject(project, AndroidConstants.MARKER_AAPT_COMPILE); + removeMarkersFromProject(project, AndroidConstants.MARKER_XML); + removeMarkersFromProject(project, AndroidConstants.MARKER_AIDL); + } + + @Override + protected void startupOnInitialize() { + super.startupOnInitialize(); + + mDerivedProgressMonitor = new DerivedProgressMonitor(); + + IProject project = getProject(); + + // load the previous IFolder and java package. + mManifestPackage = loadProjectStringProperty(PROPERTY_PACKAGE); + + // get the source folder in which all the Java files are created + mGenFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); + + // Load the current compile flags. We ask for true if not found to force a + // recompile. + mMustCompileResources = loadProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, true); + boolean mustCompileAidl = loadProjectBooleanProperty(PROPERTY_COMPILE_AIDL, true); + + // if we stored that we have to compile some aidl, we build the list that will compile them + // all + if (mustCompileAidl) { + IJavaProject javaProject = JavaCore.create(project); + ArrayList sourceFolderPathList = BaseProjectHelper.getSourceClasspaths( + javaProject); + + buildAidlCompilationList(project, sourceFolderPathList); + } + } + + /** + * Delete the a generated java class associated with the specified java package. + * @param filename Name of the generated file to remove. + * @param javaPackage the old java package + */ + private void deleteObsoleteGeneratedClass(String filename, String javaPackage) { + if (javaPackage == null) { + return; + } + + IPath packagePath = getJavaPackagePath(javaPackage); + IPath iPath = packagePath.append(filename); + + // Find a matching resource object. + IResource javaFile = mGenFolder.findMember(iPath); + if (javaFile != null && javaFile.exists() && javaFile.getType() == IResource.FILE) { + try { + // delete + javaFile.delete(true, null); + + // refresh parent + javaFile.getParent().refreshLocal(IResource.DEPTH_ONE, new NullProgressMonitor()); + + } catch (CoreException e) { + // failed to delete it, the user will have to delete it manually. + String message = String.format(Messages.Delete_Obsolete_Error, + javaFile.getFullPath()); + IProject project = getProject(); + AdtPlugin.printErrorToConsole(project, message); + AdtPlugin.printErrorToConsole(project, e.getMessage()); + } + } + } + + /** + * Creates a relative {@link IPath} from a java package. + * @param javaPackageName the java package. + */ + private IPath getJavaPackagePath(String javaPackageName) { + // convert the java package into path + String[] segments = javaPackageName.split(AndroidConstants.RE_DOT); + + StringBuilder path = new StringBuilder(); + for (String s : segments) { + path.append(AndroidConstants.WS_SEP_CHAR); + path.append(s); + } + + return new Path(path.toString()); + } + + /** + * Returns an {@link IFolder} (located inside the 'gen' source folder), that matches the + * package defined in the manifest. This {@link IFolder} may not actually exist + * (aapt will create it anyway). + * @param project The project. + * @return the {@link IFolder} that will contain the R class or null if + * the folder was not found. + * @throws CoreException + */ + private IFolder getGenManifestPackageFolder(IProject project) + throws CoreException { + // get the path for the package + IPath packagePath = getJavaPackagePath(mManifestPackage); + + // get a folder for this path under the 'gen' source folder, and return it. + // This IFolder may not reference an actual existing folder. + return mGenFolder.getFolder(packagePath); + } + + /** + * Compiles aidl files into java. This will also removes old java files + * created from aidl files that are now gone. + * @param projectTarget Target of the project + * @param sourceFolders the list of source folders, relative to the workspace. + * @param monitor the projess monitor + * @returns true if it did something + * @throws CoreException + */ + private boolean handleAidl(IAndroidTarget projectTarget, ArrayList sourceFolders, + IProgressMonitor monitor) throws CoreException { + if (mAidlToCompile.size() == 0 && mAidlToRemove.size() == 0) { + return false; + } + + // create the command line + String[] command = new String[4 + sourceFolders.size()]; + int index = 0; + command[index++] = projectTarget.getPath(IAndroidTarget.AIDL); + command[index++] = "-p" + Sdk.getCurrent().getTarget(getProject()).getPath( //$NON-NLS-1$ + IAndroidTarget.ANDROID_AIDL); + + // since the path are relative to the workspace and not the project itself, we need + // the workspace root. + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + for (IPath p : sourceFolders) { + IFolder f = wsRoot.getFolder(p); + command[index++] = "-I" + f.getLocation().toOSString(); //$NON-NLS-1$ + } + + // list of files that have failed compilation. + ArrayList stillNeedCompilation = new ArrayList(); + + // if an aidl file is being removed before we managed to compile it, it'll be in + // both list. We *need* to remove it from the compile list or it'll never go away. + for (AidlData aidlFile : mAidlToRemove) { + int pos = mAidlToCompile.indexOf(aidlFile); + if (pos != -1) { + mAidlToCompile.remove(pos); + } + } + + // loop until we've compile them all + for (AidlData aidlData : mAidlToCompile) { + // Remove the AIDL error markers from the aidl file + removeMarkersFromFile(aidlData.aidlFile, AndroidConstants.MARKER_AIDL); + + // get the path of the source file. + IPath sourcePath = aidlData.aidlFile.getLocation(); + String osSourcePath = sourcePath.toOSString(); + + IFile javaFile = getGenDestinationFile(aidlData, true /*createFolders*/, monitor); + + // finish to set the command line. + command[index] = osSourcePath; + command[index + 1] = javaFile.getLocation().toOSString(); + + // launch the process + if (execAidl(command, aidlData.aidlFile) == false) { + // aidl failed. File should be marked. We add the file to the list + // of file that will need compilation again. + stillNeedCompilation.add(aidlData); + + // and we move on to the next one. + continue; + } else { + // make sure the file will be marked as derived once we refresh the 'gen' source + // folder. + mDerivedProgressMonitor.addFile(javaFile); + } + } + + // change the list to only contains the file that have failed compilation + mAidlToCompile.clear(); + mAidlToCompile.addAll(stillNeedCompilation); + + // Remove the java files created from aidl files that have been removed. + for (AidlData aidlData : mAidlToRemove) { + IFile javaFile = getGenDestinationFile(aidlData, false /*createFolders*/, monitor); + if (javaFile.exists()) { + // This confirms the java file was generated by the builder, + // we can delete the aidlFile. + javaFile.delete(true, null); + + // Refresh parent. + javaFile.getParent().refreshLocal(IResource.DEPTH_ONE, monitor); + } + } + + mAidlToRemove.clear(); + + // store the build state. If there are any files that failed to compile, we will + // force a full aidl compile on the next project open. (unless a full compilation succeed + // before the project is closed/re-opened.) + // TODO: Optimize by saving only the files that need compilation + saveProjectBooleanProperty(PROPERTY_COMPILE_AIDL , mAidlToCompile.size() > 0); + + return true; + } + + /** + * Returns the {@link IFile} handle to the destination file for a given aild source file + * ({@link AidlData}). + * @param aidlData the data for the aidl source file. + * @param createFolders whether or not the parent folder of the destination should be created + * if it does not exist. + * @param monitor the progress monitor + * @return the handle to the destination file. + * @throws CoreException + */ + private IFile getGenDestinationFile(AidlData aidlData, boolean createFolders, + IProgressMonitor monitor) throws CoreException { + // build the destination folder path. + // Use the path of the source file, except for the path leading to its source folder, + // and for the last segment which is the filename. + int segmentToSourceFolderCount = aidlData.sourceFolder.getFullPath().segmentCount(); + IPath packagePath = aidlData.aidlFile.getFullPath().removeFirstSegments( + segmentToSourceFolderCount).removeLastSegments(1); + Path destinationPath = new Path(packagePath.toString()); + + // get an IFolder for this path. It's relative to the 'gen' folder already + IFolder destinationFolder = mGenFolder.getFolder(destinationPath); + + // create it if needed. + if (destinationFolder.exists() == false && createFolders) { + createFolder(destinationFolder, monitor); + } + + // Build the Java file name from the aidl name. + String javaName = aidlData.aidlFile.getName().replaceAll(AndroidConstants.RE_AIDL_EXT, + AndroidConstants.DOT_JAVA); + + // get the resource for the java file. + IFile javaFile = destinationFolder.getFile(javaName); + return javaFile; + } + + /** + * Creates the destination folder. Because + * {@link IFolder#create(boolean, boolean, IProgressMonitor)} only works if the parent folder + * already exists, this goes and ensure that all the parent folders actually exist, or it + * creates them as well. + * @param destinationFolder The folder to create + * @param monitor the {@link IProgressMonitor}, + * @throws CoreException + */ + private void createFolder(IFolder destinationFolder, IProgressMonitor monitor) + throws CoreException { + + // check the parent exist and create if necessary. + IContainer parent = destinationFolder.getParent(); + if (parent.getType() == IResource.FOLDER && parent.exists() == false) { + createFolder((IFolder)parent, monitor); + } + + // create the folder. + destinationFolder.create(true /*force*/, true /*local*/, + new SubProgressMonitor(monitor, 10)); + } + + /** + * Execute the aidl command line, parse the output, and mark the aidl file + * with any reported errors. + * @param command the String array containing the command line to execute. + * @param file The IFile object representing the aidl file being + * compiled. + * @return false if the exec failed, and build needs to be aborted. + */ + private boolean execAidl(String[] command, IFile file) { + // do the exec + try { + Process p = Runtime.getRuntime().exec(command); + + // list to store each line of stderr + ArrayList results = new ArrayList(); + + // get the output and return code from the process + int result = grabProcessOutput(p, results); + + // attempt to parse the error output + boolean error = parseAidlOutput(results, file); + + // If the process failed and we couldn't parse the output + // we pring a message, mark the project and exit + if (result != 0 && error == true) { + // display the message in the console. + AdtPlugin.printErrorToConsole(getProject(), results.toArray()); + + // mark the project and exit + markProject(AndroidConstants.MARKER_ADT, Messages.Unparsed_AIDL_Errors, + IMarker.SEVERITY_ERROR); + return false; + } + } catch (IOException e) { + // mark the project and exit + String msg = String.format(Messages.AIDL_Exec_Error, command[0]); + markProject(AndroidConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (InterruptedException e) { + // mark the project and exit + String msg = String.format(Messages.AIDL_Exec_Error, command[0]); + markProject(AndroidConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } + + return true; + } + + /** + * Goes through the build paths and fills the list of aidl files to compile + * ({@link #mAidlToCompile}). + * @param project The project. + * @param sourceFolderPathList The list of source folder paths. + */ + private void buildAidlCompilationList(IProject project, + ArrayList sourceFolderPathList) { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + for (IPath sourceFolderPath : sourceFolderPathList) { + IFolder sourceFolder = root.getFolder(sourceFolderPath); + // we don't look in the 'gen' source folder as there will be no source in there. + if (sourceFolder.exists() && sourceFolder.equals(mGenFolder) == false) { + scanFolderForAidl(sourceFolder, sourceFolder); + } + } + } + + /** + * Scans a folder and fills the list of aidl files to compile. + * @param sourceFolder the root source folder. + * @param folder The folder to scan. + */ + private void scanFolderForAidl(IFolder sourceFolder, IFolder folder) { + try { + IResource[] members = folder.members(); + for (IResource r : members) { + // get the type of the resource + switch (r.getType()) { + case IResource.FILE: + // if this a file, check that the file actually exist + // and that it's an aidl file + if (r.exists() && + AndroidConstants.EXT_AIDL.equalsIgnoreCase(r.getFileExtension())) { + mAidlToCompile.add(new AidlData(sourceFolder, (IFile)r)); + } + break; + case IResource.FOLDER: + // recursively go through children + scanFolderForAidl(sourceFolder, (IFolder)r); + break; + default: + // this would mean it's a project or the workspace root + // which is unlikely to happen. we do nothing + break; + } + } + } catch (CoreException e) { + // Couldn't get the members list for some reason. Just return. + } + } + + + /** + * Parse the output of aidl and mark the file with any errors. + * @param lines The output to parse. + * @param file The file to mark with error. + * @return true if the parsing failed, false if success. + */ + private boolean parseAidlOutput(ArrayList lines, IFile file) { + // nothing to parse? just return false; + if (lines.size() == 0) { + return false; + } + + Matcher m; + + for (int i = 0; i < lines.size(); i++) { + String p = lines.get(i); + + m = sAidlPattern1.matcher(p); + if (m.matches()) { + // we can ignore group 1 which is the location since we already + // have a IFile object representing the aidl file. + String lineStr = m.group(2); + String msg = m.group(3); + + // get the line number + int line = 0; + try { + line = Integer.parseInt(lineStr); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid + // file number. Parsing failed and we return true + return true; + } + + // mark the file + BaseProjectHelper.markResource(file, AndroidConstants.MARKER_AIDL, msg, line, + IMarker.SEVERITY_ERROR); + + // success, go to the next line + continue; + } + + // invalid line format, flag as error, and bail + return true; + } + + return false; + } + + /** + * Merge the current list of aidl file to compile/remove with the new one. + * @param toCompile List of file to compile + * @param toRemove List of file to remove + */ + private void mergeAidlFileModifications(ArrayList toCompile, + ArrayList toRemove) { + // loop through the new toRemove list, and add it to the old one, + // plus remove any file that was still to compile and that are now + // removed + for (AidlData r : toRemove) { + if (mAidlToRemove.indexOf(r) == -1) { + mAidlToRemove.add(r); + } + + int index = mAidlToCompile.indexOf(r); + if (index != -1) { + mAidlToCompile.remove(index); + } + } + + // now loop through the new files to compile and add it to the list. + // Also look for them in the remove list, this would mean that they + // were removed, then added back, and we shouldn't remove them, just + // recompile them. + for (AidlData r : toCompile) { + if (mAidlToCompile.indexOf(r) == -1) { + mAidlToCompile.add(r); + } + + int index = mAidlToRemove.indexOf(r); + if (index != -1) { + mAidlToRemove.remove(index); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerDeltaVisitor.java new file mode 100644 index 000000000..f09bace77 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerDeltaVisitor.java @@ -0,0 +1,540 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.build; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.build.BaseBuilder.BaseDeltaVisitor; +import com.android.ide.eclipse.adt.internal.build.PreCompilerBuilder.AidlData; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestParser; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; + +import java.util.ArrayList; + +/** + * Resource Delta visitor for the pre-compiler. + *

This delta visitor only cares about files that are the source or the result of actions of the + * {@link PreCompilerBuilder}: + *

  • R.java/Manifest.java generated by compiling the resources
  • + *
  • Any Java files generated by aidl
. + * + * Therefore it looks for the following: + *